You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by fj...@apache.org on 2019/07/01 02:33:23 UTC

[incubator-druid] branch master updated: Web console: Improve data loader styling, enforce stricter TS types (#8001)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new f16f13c  Web console: Improve data loader styling, enforce stricter TS types (#8001)
f16f13c is described below

commit f16f13cf619d52aee97d2f8229fa60ffd9c818f6
Author: Vadim Ogievetsky <va...@gmail.com>
AuthorDate: Sun Jun 30 19:33:16 2019 -0700

    Web console: Improve data loader styling, enforce stricter TS types (#8001)
    
    * add assets to auth exclude path
    
    * add frame to tile page
    
    * better empty filter state
    
    * strict TS
    
    * fix segments go to sql
    
    * add unavailable segments
    
    * factor out sugestable input
    
    * fix tests
    
    * update datasources sql
    
    * no depricated extend
    
    * add index spec to tuning configs
    
    * fix scss lint
---
 .../druid/cli/RouterJettyServerInitializer.java    |   1 +
 web-console/package.json                           |   4 +-
 web-console/script/mkcomp                          |   4 +-
 .../bootstrap/react-table-custom-pagination.tsx    |   2 +-
 web-console/src/bootstrap/react-table-defaults.tsx |   2 +-
 .../src/components/action-cell/action-cell.tsx     |   2 +-
 .../src/components/action-icon/action-icon.tsx     |   2 +-
 .../__snapshots__/auto-form.spec.tsx.snap          |   2 +-
 .../src/components/auto-form/auto-form.spec.tsx    |   2 +-
 web-console/src/components/auto-form/auto-form.tsx |  68 +---
 .../components/center-message/center-message.tsx   |   2 +-
 .../__snapshots__/clearable-input.spec.tsx.snap    |   2 +-
 .../clearable-input/clearable-input.spec.tsx       |   6 +-
 .../components/clearable-input/clearable-input.tsx |   2 +-
 .../src/components/external-link/external-link.tsx |   2 +-
 .../src/components/header-bar/header-bar.tsx       |   7 +-
 .../src/components/json-collapse/json-collapse.tsx |   3 +-
 .../src/components/json-input/json-input.spec.tsx  |   2 +-
 .../src/components/json-input/json-input.tsx       |   2 +-
 web-console/src/components/loader/loader.tsx       |   2 +-
 .../components/refresh-button/refresh-button.tsx   |   6 +-
 .../components/rule-editor/rule-editor.spec.tsx    |   4 +-
 .../src/components/rule-editor/rule-editor.tsx     |   5 +-
 web-console/src/components/show-json/show-json.tsx |   4 +-
 web-console/src/components/show-log/show-log.tsx   |   4 +-
 .../__snapshots__/suggestible-input.spec.tsx.snap  |  51 +++
 .../suggestible-input/suggestible-input.spec.tsx}  |  11 +-
 .../suggestible-input/suggestible-input.tsx        | 106 ++++++
 .../src/components/table-cell/table-cell.tsx       |   5 +-
 .../table-column-selector.spec.tsx                 |   2 +-
 .../table-column-selector.tsx                      |   4 +-
 .../src/components/timed-button/timed-button.scss  |   5 +-
 web-console/src/console-application.tsx            |  46 +--
 .../src/dialogs/about-dialog/about-dialog.spec.tsx |   4 +-
 .../src/dialogs/about-dialog/about-dialog.tsx      |   2 +-
 .../async-action-dialog.spec.tsx                   |   6 +-
 .../async-action-dialog/async-action-dialog.tsx    |  15 +-
 .../__snapshots__/compaction-dialog.spec.tsx.snap  |   2 +-
 .../compaction-dialog/compaction-dialog.spec.tsx   |   6 +-
 .../compaction-dialog/compaction-dialog.tsx        |   2 +-
 ...oordinator-dynamic-config-dialog.spec.tsx.snap} |  12 +-
 .../coordinator-dynamic-config-dialog.scss}        |   2 +-
 .../coordinator-dynamic-config-dialog.spec.tsx}    |   6 +-
 .../coordinator-dynamic-config-dialog.tsx}         |  22 +-
 .../dialogs/history-dialog/history-dialog.spec.tsx |   4 +-
 .../src/dialogs/history-dialog/history-dialog.tsx  |   6 +-
 web-console/src/dialogs/index.ts                   |   4 +-
 .../lookup-edit-dialog/lookup-edit-dialog.spec.tsx |   4 +-
 .../lookup-edit-dialog/lookup-edit-dialog.tsx      |   5 +-
 .../overload-dynamic-config-dialog.spec.tsx.snap}  |  20 +-
 .../overload-dynamic-config-dialog.spec.tsx}       |  11 +-
 .../overlord-dynamic-config-dialog.scss}           |   2 +-
 .../overlord-dynamic-config-dialog.tsx}            |  18 +-
 .../overload-dynamic-config.spec.tsx.snap          | 356 ---------------------
 .../overload-dynamic-config.spec.tsx               |  43 ---
 .../query-plan-dialog/query-plan-dialog.spec.tsx   |   4 +-
 .../query-plan-dialog/query-plan-dialog.tsx        |   2 +-
 .../__snapshots__/retention-dialog.spec.tsx.snap   |  10 -
 .../retention-dialog.array.spec.ts                 |  56 ----
 .../retention-dialog/retention-dialog.spec.tsx     |  41 ++-
 .../dialogs/retention-dialog/retention-dialog.tsx  |  18 +-
 .../segment-table-action-dialog.spec.tsx.snap      |  13 +-
 .../segment-table-action-dialog.spec.tsx           |   7 +-
 .../segment-table-action-dialog.tsx                |   2 +-
 .../__snapshots__/show-value-dialog.spec.tsx.snap  |   2 +-
 .../show-value-dialog/show-value-dialog.scss       |   9 +-
 .../show-value-dialog/show-value-dialog.spec.tsx   |   4 +-
 .../show-value-dialog/show-value-dialog.tsx        |  31 +-
 .../dialogs/snitch-dialog/snitch-dialog.spec.tsx   |   4 +-
 .../src/dialogs/snitch-dialog/snitch-dialog.tsx    |  26 +-
 .../src/dialogs/spec-dialog/spec-dialog.spec.tsx   |   8 +-
 .../src/dialogs/spec-dialog/spec-dialog.tsx        |   5 +-
 .../supervisor-table-action-dialog.spec.tsx        |   4 +-
 .../table-action-dialog/table-action-dialog.scss   |   2 +-
 .../table-action-dialog.spec.tsx                   |   4 +-
 .../table-action-dialog/table-action-dialog.tsx    |   4 +-
 .../task-table-action-dialog.spec.tsx              |   4 +-
 web-console/src/utils/general.tsx                  |  11 +-
 web-console/src/utils/ingestion-spec.tsx           |  49 ++-
 .../src/utils/local-storage-backed-array.tsx       |  15 +-
 web-console/src/utils/local-storage-keys.tsx       |   1 +
 web-console/src/utils/query-manager.tsx            |   7 +-
 .../__snapshots__/datasource-view.spec.tsx.snap    |   9 +
 .../views/datasource-view/datasource-view.spec.tsx |   6 +-
 .../src/views/datasource-view/datasource-view.tsx  | 106 +++---
 web-console/src/views/home-view/home-view.tsx      | 122 +++----
 .../__snapshots__/load-data-view.spec.tsx.snap     |   2 +-
 .../load-data-view/filter-table/filter-table.tsx   |   8 +-
 .../src/views/load-data-view/load-data-view.scss   |   9 +-
 .../views/load-data-view/load-data-view.spec.tsx   |   2 +-
 .../src/views/load-data-view/load-data-view.tsx    |  59 +++-
 .../parse-data-table/parse-data-table.tsx          |   2 +-
 .../parse-time-table/parse-time-table.scss         |   1 +
 .../parse-time-table/parse-time-table.tsx          |   2 +-
 .../load-data-view/schema-table/schema-table.tsx   |  12 +-
 .../transform-table/transform-table.tsx            |   6 +-
 .../__snapshots__/lookups-view.spec.tsx.snap       |  15 +-
 .../src/views/lookups-view/lookups-view.tsx        |  46 ++-
 .../views/query-view/column-tree/column-tree.tsx   |   8 +-
 .../query-extra-info/query-extra-info.tsx          |   2 +-
 .../views/query-view/query-input/query-input.tsx   |   8 +-
 .../views/query-view/query-output/query-output.tsx |   2 +-
 web-console/src/views/query-view/query-view.tsx    |  18 +-
 .../src/views/query-view/run-button/run-button.tsx |   2 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |   2 +-
 .../src/views/segments-view/segments-view.spec.tsx |   2 +-
 .../src/views/segments-view/segments-view.tsx      |  45 +--
 .../src/views/servers-view/servers-view.spec.tsx   |   4 +-
 .../src/views/servers-view/servers-view.tsx        | 108 +++----
 .../src/views/task-view/tasks-view.spec.tsx        |   4 +-
 web-console/src/views/task-view/tasks-view.tsx     | 115 ++++---
 web-console/tsconfig.json                          |   7 +-
 web-console/tslint.json                            |   1 -
 web-console/unified-console.html                   |   2 +-
 web-console/webpack.config.js                      |  20 +-
 115 files changed, 876 insertions(+), 1148 deletions(-)

diff --git a/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java b/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
index b206e4b..a336ae6 100644
--- a/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
+++ b/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
@@ -65,6 +65,7 @@ public class RouterJettyServerInitializer implements JettyServerInitializer
   protected static final List<String> UNSECURED_PATHS_FOR_UI = ImmutableList.of(
       "/",
       "/coordinator-console/*",
+      "/assets/*",
       "/public/*",
       "/old-console/*",
       "/pages/*",
diff --git a/web-console/package.json b/web-console/package.json
index a4369d6..edb85f2 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -1,6 +1,6 @@
 {
   "name": "web-console",
-  "version": "0.15.0",
+  "version": "0.16.0",
   "description": "A web console for Apache Druid",
   "author": "Imply Data Inc.",
   "license": "Apache-2.0",
@@ -37,7 +37,7 @@
     "compile": "./script/build",
     "pretest": "./script/build",
     "run": "./script/run",
-    "test": "npm run tslint && jest --silent 2>&1",
+    "test": "npm run tslint && npm run stylelint && jest --silent 2>&1",
     "coverage": "jest --coverage",
     "update-snapshots": "jest -u",
     "tslint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter 'src/**/*.ts?(x)'",
diff --git a/web-console/script/mkcomp b/web-console/script/mkcomp
index 5c60a80..5634959 100755
--- a/web-console/script/mkcomp
+++ b/web-console/script/mkcomp
@@ -85,7 +85,7 @@ import React from 'react';
 
 import './${name}.scss';
 
-export interface ${camelName}Props extends React.Props<any> {
+export interface ${camelName}Props {
 }
 
 export interface ${camelName}State {
@@ -100,7 +100,7 @@ export class ${camelName} extends React.PureComponent<${camelName}Props, ${camel
 
   render() {
     return <div className="${name}">
-    
+      Stuff...
     </div>;
   }
 }
diff --git a/web-console/src/bootstrap/react-table-custom-pagination.tsx b/web-console/src/bootstrap/react-table-custom-pagination.tsx
index 5c53b8e..5be343df 100644
--- a/web-console/src/bootstrap/react-table-custom-pagination.tsx
+++ b/web-console/src/bootstrap/react-table-custom-pagination.tsx
@@ -21,7 +21,7 @@ import React from 'react';
 
 import './react-table-custom-pagination.scss';
 
-interface ReactTableCustomPaginationProps extends React.Props<any> {
+interface ReactTableCustomPaginationProps {
   pages: number;
   page: number;
   showPageSizeOptions: boolean;
diff --git a/web-console/src/bootstrap/react-table-defaults.tsx b/web-console/src/bootstrap/react-table-defaults.tsx
index 15c09cd..f0d2b2f 100644
--- a/web-console/src/bootstrap/react-table-defaults.tsx
+++ b/web-console/src/bootstrap/react-table-defaults.tsx
@@ -38,7 +38,7 @@ class NoData extends React.PureComponent {
 
 Object.assign(ReactTableDefaults, {
   className: '-striped -highlight',
-  defaultFilterMethod: (filter: Filter, row: any, column: any) => {
+  defaultFilterMethod: (filter: Filter, row: any) => {
     const id = filter.pivotId || filter.id;
     return booleanCustomTableFilter(filter, row[id]);
   },
diff --git a/web-console/src/components/action-cell/action-cell.tsx b/web-console/src/components/action-cell/action-cell.tsx
index 0bd7e79..32c2fa2 100644
--- a/web-console/src/components/action-cell/action-cell.tsx
+++ b/web-console/src/components/action-cell/action-cell.tsx
@@ -25,7 +25,7 @@ import { ActionIcon } from '../action-icon/action-icon';
 
 import './action-cell.scss';
 
-export interface ActionCellProps extends React.Props<any> {
+export interface ActionCellProps {
   onDetail?: () => void;
   actions?: BasicAction[];
 }
diff --git a/web-console/src/components/action-icon/action-icon.tsx b/web-console/src/components/action-icon/action-icon.tsx
index 36b4b4e..c3c7da2 100644
--- a/web-console/src/components/action-icon/action-icon.tsx
+++ b/web-console/src/components/action-icon/action-icon.tsx
@@ -22,7 +22,7 @@ import React from 'react';
 
 import './action-icon.scss';
 
-export interface ActionIconProps extends React.Props<any> {
+export interface ActionIconProps {
   className?: string;
   icon: IconName;
   onClick?: () => void;
diff --git a/web-console/src/components/auto-form/__snapshots__/auto-form.spec.tsx.snap b/web-console/src/components/auto-form/__snapshots__/auto-form.spec.tsx.snap
index df8004a..b95c63c 100644
--- a/web-console/src/components/auto-form/__snapshots__/auto-form.spec.tsx.snap
+++ b/web-console/src/components/auto-form/__snapshots__/auto-form.spec.tsx.snap
@@ -190,7 +190,7 @@ exports[`auto-form snapshot matches snapshot 1`] = `
       class="bp3-form-content"
     >
       <div
-        class="bp3-input-group"
+        class="bp3-input-group suggestible-input"
       >
         <input
           class="bp3-input"
diff --git a/web-console/src/components/auto-form/auto-form.spec.tsx b/web-console/src/components/auto-form/auto-form.spec.tsx
index 87ad211..c0b4a96 100644
--- a/web-console/src/components/auto-form/auto-form.spec.tsx
+++ b/web-console/src/components/auto-form/auto-form.spec.tsx
@@ -35,7 +35,7 @@ describe('auto-form snapshot', () => {
           { name: 'testSeven', type: 'json' },
         ]}
         model={String}
-        onChange={(newModel: Record<string, any>) => {}}
+        onChange={() => {}}
       />
     );
     const { container } = render(autoForm);
diff --git a/web-console/src/components/auto-form/auto-form.tsx b/web-console/src/components/auto-form/auto-form.tsx
index 85ee1de..e7db357 100644
--- a/web-console/src/components/auto-form/auto-form.tsx
+++ b/web-console/src/components/auto-form/auto-form.tsx
@@ -16,32 +16,17 @@
  * limitations under the License.
  */
 
-import {
-  Button,
-  FormGroup,
-  HTMLSelect,
-  Icon,
-  InputGroup,
-  Menu,
-  MenuItem,
-  NumericInput,
-  Popover,
-  Position,
-} from '@blueprintjs/core';
+import { FormGroup, HTMLSelect, Icon, NumericInput, Popover } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
 import { deepDelete, deepGet, deepSet } from '../../utils/object-change';
 import { ArrayInput } from '../array-input/array-input';
 import { JSONInput } from '../json-input/json-input';
+import { SuggestibleInput, SuggestionGroup } from '../suggestible-input/suggestible-input';
 
 import './auto-form.scss';
 
-export interface SuggestionGroup {
-  group: string;
-  suggestions: string[];
-}
-
 export interface Field<T> {
   name: string;
   label?: string;
@@ -55,7 +40,7 @@ export interface Field<T> {
   min?: number;
 }
 
-export interface AutoFormProps<T> extends React.Props<any> {
+export interface AutoFormProps<T> {
   fields: Field<T>[];
   model: T | null;
   onChange: (newModel: T) => void;
@@ -64,13 +49,13 @@ export interface AutoFormProps<T> extends React.Props<any> {
   large?: boolean;
 }
 
-export interface AutoFormState<T> {
+export interface AutoFormState {
   jsonInputsValidity: any;
 }
 
 export class AutoForm<T extends Record<string, any>> extends React.PureComponent<
   AutoFormProps<T>,
-  AutoFormState<T>
+  AutoFormState
 > {
   static makeLabelName(label: string): string {
     let newLabel = label
@@ -159,42 +144,11 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
   private renderStringInput(field: Field<T>, sanitize?: (str: string) => string): JSX.Element {
     const { model, large } = this.props;
 
-    const suggestionsMenu = field.suggestions ? (
-      <Menu>
-        {field.suggestions.map(suggestion => {
-          if (typeof suggestion === 'string') {
-            return (
-              <MenuItem
-                key={suggestion}
-                text={suggestion}
-                onClick={() => this.fieldChange(field, suggestion)}
-              />
-            );
-          } else {
-            return (
-              <MenuItem key={suggestion.group} text={suggestion.group}>
-                {suggestion.suggestions.map(suggestion => (
-                  <MenuItem
-                    key={suggestion}
-                    text={suggestion}
-                    onClick={() => this.fieldChange(field, suggestion)}
-                  />
-                ))}
-              </MenuItem>
-            );
-          }
-        })}
-      </Menu>
-    ) : (
-      undefined
-    );
-
     const modalValue = deepGet(model as any, field.name);
     return (
-      <InputGroup
+      <SuggestibleInput
         value={modalValue != null ? modalValue : field.defaultValue || ''}
-        onChange={(e: any) => {
-          let v = e.target.value;
+        onValueChange={v => {
           if (sanitize) v = sanitize(v);
           this.fieldChange(field, v);
         }}
@@ -202,13 +156,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
           if (modalValue === '') this.fieldChange(field, undefined);
         }}
         placeholder={field.placeholder}
-        rightElement={
-          suggestionsMenu && (
-            <Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>
-              <Button icon={IconNames.CARET_DOWN} minimal />
-            </Popover>
-          )
-        }
+        suggestions={field.suggestions}
         large={large}
         disabled={field.disabled}
       />
diff --git a/web-console/src/components/center-message/center-message.tsx b/web-console/src/components/center-message/center-message.tsx
index c961cf2..b84e0cf 100644
--- a/web-console/src/components/center-message/center-message.tsx
+++ b/web-console/src/components/center-message/center-message.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 
 import './center-message.scss';
 
-export interface CenterMessageProps extends React.Props<any> {}
+export interface CenterMessageProps {}
 
 export class CenterMessage extends React.PureComponent<CenterMessageProps> {
   render() {
diff --git a/web-console/src/components/clearable-input/__snapshots__/clearable-input.spec.tsx.snap b/web-console/src/components/clearable-input/__snapshots__/clearable-input.spec.tsx.snap
index 8f6e315..8e59dc6 100644
--- a/web-console/src/components/clearable-input/__snapshots__/clearable-input.spec.tsx.snap
+++ b/web-console/src/components/clearable-input/__snapshots__/clearable-input.spec.tsx.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`decribe clearable-input matches snapshot 1`] = `
+exports[`clearable-input matches snapshot 1`] = `
 <div
   class="bp3-input-group clearable-input testClassName"
 >
diff --git a/web-console/src/components/clearable-input/clearable-input.spec.tsx b/web-console/src/components/clearable-input/clearable-input.spec.tsx
index 289d2ee..ef44701 100644
--- a/web-console/src/components/clearable-input/clearable-input.spec.tsx
+++ b/web-console/src/components/clearable-input/clearable-input.spec.tsx
@@ -21,16 +21,16 @@ import { render } from 'react-testing-library';
 
 import { ClearableInput } from './clearable-input';
 
-describe('decribe clearable-input', () => {
+describe('clearable-input', () => {
   it('matches snapshot', () => {
     const centerMessage = (
       <ClearableInput
         className={'testClassName'}
         value={'testValue'}
         placeholder={'testPlaceholder'}
-        onChange={(value: string) => null}
+        onChange={() => null}
       >
-        ;<div>Hello World</div>
+        <div>Hello World</div>
       </ClearableInput>
     );
 
diff --git a/web-console/src/components/clearable-input/clearable-input.tsx b/web-console/src/components/clearable-input/clearable-input.tsx
index 0cbe36c..1376759 100644
--- a/web-console/src/components/clearable-input/clearable-input.tsx
+++ b/web-console/src/components/clearable-input/clearable-input.tsx
@@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import React from 'react';
 
-export interface ClearableInputProps extends React.Props<any> {
+export interface ClearableInputProps {
   className?: string;
   value: string;
   onChange: (value: string) => void;
diff --git a/web-console/src/components/external-link/external-link.tsx b/web-console/src/components/external-link/external-link.tsx
index 3b84e4e..f4ca4b0 100644
--- a/web-console/src/components/external-link/external-link.tsx
+++ b/web-console/src/components/external-link/external-link.tsx
@@ -18,7 +18,7 @@
 
 import React from 'react';
 
-export interface ExternalLinkProps extends React.Props<any> {
+export interface ExternalLinkProps {
   href: string;
 }
 
diff --git a/web-console/src/components/header-bar/header-bar.tsx b/web-console/src/components/header-bar/header-bar.tsx
index 054fc4b..5d4bc15 100644
--- a/web-console/src/components/header-bar/header-bar.tsx
+++ b/web-console/src/components/header-bar/header-bar.tsx
@@ -22,7 +22,6 @@ import {
   Button,
   Intent,
   Menu,
-  MenuDivider,
   MenuItem,
   Navbar,
   NavbarDivider,
@@ -34,8 +33,8 @@ import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
 import { AboutDialog } from '../../dialogs/about-dialog/about-dialog';
-import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config/coordinator-dynamic-config';
-import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config/overlord-dynamic-config';
+import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
+import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
 import {
   DRUID_DOCS,
   DRUID_GITHUB,
@@ -56,7 +55,7 @@ export type HeaderActiveTab =
   | 'servers'
   | 'lookups';
 
-export interface HeaderBarProps extends React.Props<any> {
+export interface HeaderBarProps {
   active: HeaderActiveTab;
   hideLegacy: boolean;
 }
diff --git a/web-console/src/components/json-collapse/json-collapse.tsx b/web-console/src/components/json-collapse/json-collapse.tsx
index afc7a09..0441d78 100644
--- a/web-console/src/components/json-collapse/json-collapse.tsx
+++ b/web-console/src/components/json-collapse/json-collapse.tsx
@@ -17,10 +17,9 @@
  */
 
 import { Button, Collapse, TextArea } from '@blueprintjs/core';
-import classNames from 'classnames';
 import React from 'react';
 
-interface JSONCollapseProps extends React.Props<any> {
+interface JSONCollapseProps {
   stringValue: string;
   buttonText: string;
 }
diff --git a/web-console/src/components/json-input/json-input.spec.tsx b/web-console/src/components/json-input/json-input.spec.tsx
index 4fd1266..c723186 100644
--- a/web-console/src/components/json-input/json-input.spec.tsx
+++ b/web-console/src/components/json-input/json-input.spec.tsx
@@ -23,7 +23,7 @@ import { JSONInput } from './json-input';
 
 describe('json input', () => {
   it('matches snapshot', () => {
-    const jsonCollapse = <JSONInput onChange={(newJSONValue: any) => {}} value={'test'} />;
+    const jsonCollapse = <JSONInput onChange={() => {}} value={'test'} />;
     const { container } = render(jsonCollapse);
     expect(container.firstChild).toMatchSnapshot();
   });
diff --git a/web-console/src/components/json-input/json-input.tsx b/web-console/src/components/json-input/json-input.tsx
index 975b52f..0aff544 100644
--- a/web-console/src/components/json-input/json-input.tsx
+++ b/web-console/src/components/json-input/json-input.tsx
@@ -21,7 +21,7 @@ import AceEditor from 'react-ace';
 
 import { parseStringToJSON, stringifyJSON, validJson } from '../../utils';
 
-interface JSONInputProps extends React.Props<any> {
+interface JSONInputProps {
   onChange: (newJSONValue: any) => void;
   value: any;
   updateInputValidity?: (valueValid: boolean) => void;
diff --git a/web-console/src/components/loader/loader.tsx b/web-console/src/components/loader/loader.tsx
index c6dc529..69d74be 100644
--- a/web-console/src/components/loader/loader.tsx
+++ b/web-console/src/components/loader/loader.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 
 import './loader.scss';
 
-export interface LoaderProps extends React.Props<any> {
+export interface LoaderProps {
   loadingText?: string;
   loading?: boolean; // This is needed so that this component can be used as a LoadingComponent in react table
 }
diff --git a/web-console/src/components/refresh-button/refresh-button.tsx b/web-console/src/components/refresh-button/refresh-button.tsx
index 71905b6..ab945e3 100644
--- a/web-console/src/components/refresh-button/refresh-button.tsx
+++ b/web-console/src/components/refresh-button/refresh-button.tsx
@@ -21,7 +21,7 @@ import React from 'react';
 import { LocalStorageKeys } from '../../utils';
 import { TimedButton } from '../timed-button/timed-button';
 
-export interface RefreshButtonProps extends React.Props<any> {
+export interface RefreshButtonProps {
   onRefresh: (auto: boolean) => void;
   localStorageKey?: LocalStorageKeys;
 }
@@ -45,10 +45,10 @@ export class RefreshButton extends React.PureComponent<RefreshButtonProps> {
     return (
       <TimedButton
         defaultValue={30000}
-        label={'Auto refresh every:'}
+        label="Auto refresh every:"
         intervals={intervals}
         icon={IconNames.REFRESH}
-        text={'Refresh'}
+        text="Refresh"
         onRefresh={onRefresh}
         localStorageKey={localStorageKey}
       />
diff --git a/web-console/src/components/rule-editor/rule-editor.spec.tsx b/web-console/src/components/rule-editor/rule-editor.spec.tsx
index 5f17209..d1eeefd 100644
--- a/web-console/src/components/rule-editor/rule-editor.spec.tsx
+++ b/web-console/src/components/rule-editor/rule-editor.spec.tsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { Rule, RuleEditor } from './rule-editor';
+import { RuleEditor } from './rule-editor';
 
 describe('rule editor', () => {
   it('matches snapshot', () => {
@@ -27,7 +27,7 @@ describe('rule editor', () => {
       <RuleEditor
         rule={{ type: 'loadForever' }}
         tiers={['test', 'test', 'test']}
-        onChange={(newRule: Rule) => null}
+        onChange={() => null}
         onDelete={() => null}
         moveUp={null}
         moveDown={null}
diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx
index 9cc79ca..f46a42f 100644
--- a/web-console/src/components/rule-editor/rule-editor.tsx
+++ b/web-console/src/components/rule-editor/rule-editor.tsx
@@ -28,7 +28,6 @@ import {
   TagInput,
 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import axios from 'axios';
 import React from 'react';
 
 import './rule-editor.scss';
@@ -53,7 +52,7 @@ export interface Rule {
 export type LoadType = 'load' | 'drop' | 'broadcast';
 export type TimeType = 'Forever' | 'ByInterval' | 'ByPeriod';
 
-export interface RuleEditorProps extends React.Props<any> {
+export interface RuleEditorProps {
   rule: Rule;
   tiers: any[];
   onChange: (newRule: Rule) => void;
@@ -241,7 +240,7 @@ export class RuleEditor extends React.PureComponent<RuleEditorProps, RuleEditorS
   }
 
   render() {
-    const { tiers, onChange, rule, onDelete, moveUp, moveDown } = this.props;
+    const { onChange, rule, onDelete, moveUp, moveDown } = this.props;
     const { isOpen } = this.state;
 
     if (!rule) return null;
diff --git a/web-console/src/components/show-json/show-json.tsx b/web-console/src/components/show-json/show-json.tsx
index 27b8fc3..7b8e2cc 100644
--- a/web-console/src/components/show-json/show-json.tsx
+++ b/web-console/src/components/show-json/show-json.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, ButtonGroup, InputGroup, Intent, TextArea } from '@blueprintjs/core';
+import { Button, ButtonGroup, Intent, TextArea } from '@blueprintjs/core';
 import axios from 'axios';
 import copy from 'copy-to-clipboard';
 import React from 'react';
@@ -27,7 +27,7 @@ import { downloadFile } from '../../utils';
 
 import './show-json.scss';
 
-export interface ShowJsonProps extends React.Props<any> {
+export interface ShowJsonProps {
   endpoint: string;
   transform?: (x: any) => any;
   downloadFilename?: string;
diff --git a/web-console/src/components/show-log/show-log.tsx b/web-console/src/components/show-log/show-log.tsx
index 5d6492b..8efd2b0 100644
--- a/web-console/src/components/show-log/show-log.tsx
+++ b/web-console/src/components/show-log/show-log.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, ButtonGroup, Checkbox, InputGroup, Intent, TextArea } from '@blueprintjs/core';
+import { Button, ButtonGroup, Checkbox, Intent } from '@blueprintjs/core';
 import axios from 'axios';
 import copy from 'copy-to-clipboard';
 import React from 'react';
@@ -35,7 +35,7 @@ function removeFirstPartialLine(log: string): string {
   return lines.join('\n');
 }
 
-export interface ShowLogProps extends React.Props<any> {
+export interface ShowLogProps {
   endpoint: string;
   downloadFilename?: string;
   tailOffset?: number;
diff --git a/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap b/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
new file mode 100644
index 0000000..9399672
--- /dev/null
+++ b/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`suggestible input matches snapshot 1`] = `
+<div
+  class="bp3-input-group suggestible-input"
+>
+  <input
+    class="bp3-input"
+    style="padding-right: 0px;"
+    suggestions="a,b,c"
+    type="text"
+    value=""
+  />
+  <span
+    class="bp3-input-action"
+  >
+    <span
+      class="bp3-popover-wrapper"
+    >
+      <span
+        class="bp3-popover-target"
+      >
+        <button
+          class="bp3-button bp3-minimal"
+          type="button"
+        >
+          <span
+            class="bp3-icon bp3-icon-caret-down"
+            icon="caret-down"
+          >
+            <svg
+              data-icon="caret-down"
+              height="16"
+              viewBox="0 0 16 16"
+              width="16"
+            >
+              <desc>
+                caret-down
+              </desc>
+              <path
+                d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 0 0-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+                fill-rule="evenodd"
+              />
+            </svg>
+          </span>
+        </button>
+      </span>
+    </span>
+  </span>
+</div>
+`;
diff --git a/web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx b/web-console/src/components/suggestible-input/suggestible-input.spec.tsx
similarity index 78%
copy from web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx
copy to web-console/src/components/suggestible-input/suggestible-input.spec.tsx
index 35d7214..ddc48c1 100644
--- a/web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx
+++ b/web-console/src/components/suggestible-input/suggestible-input.spec.tsx
@@ -19,14 +19,15 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { SpecDialog } from './spec-dialog';
+import { SuggestibleInput } from './suggestible-input';
 
-describe('spec dialog', () => {
+describe('suggestible input', () => {
   it('matches snapshot', () => {
-    const specDialog = (
-      <SpecDialog onSubmit={(spec: JSON) => null} onClose={() => null} title={'test'} />
+    const suggestibleInput = (
+      <SuggestibleInput onValueChange={() => {}} suggestions={['a', 'b', 'c']} />
     );
-    const { container } = render(specDialog, { container: document.body });
+
+    const { container } = render(suggestibleInput);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/components/suggestible-input/suggestible-input.tsx b/web-console/src/components/suggestible-input/suggestible-input.tsx
new file mode 100644
index 0000000..47eb174
--- /dev/null
+++ b/web-console/src/components/suggestible-input/suggestible-input.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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,
+  HTMLInputProps,
+  InputGroup,
+  Menu,
+  MenuItem,
+  Popover,
+  Position,
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import classNames from 'classnames';
+import React from 'react';
+
+export interface SuggestionGroup {
+  group: string;
+  suggestions: string[];
+}
+
+export interface SuggestibleInputProps extends HTMLInputProps {
+  onValueChange: (newValue: string) => void;
+  suggestions?: (string | SuggestionGroup)[];
+  large?: boolean;
+}
+
+export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps> {
+  constructor(props: SuggestibleInputProps, context: any) {
+    super(props, context);
+    // this.state = {};
+  }
+
+  renderSuggestionsMenu() {
+    const { suggestions, onValueChange } = this.props;
+    if (!suggestions) return undefined;
+
+    return (
+      <Menu>
+        {suggestions.map(suggestion => {
+          if (typeof suggestion === 'string') {
+            return (
+              <MenuItem
+                key={suggestion}
+                text={suggestion}
+                onClick={() => onValueChange(suggestion)}
+              />
+            );
+          } else {
+            return (
+              <MenuItem key={suggestion.group} text={suggestion.group}>
+                {suggestion.suggestions.map(suggestion => (
+                  <MenuItem
+                    key={suggestion}
+                    text={suggestion}
+                    onClick={() => onValueChange(suggestion)}
+                  />
+                ))}
+              </MenuItem>
+            );
+          }
+        })}
+      </Menu>
+    );
+  }
+
+  render() {
+    const { className, value, defaultValue, onValueChange, large, ...rest } = this.props;
+    const suggestionsMenu = this.renderSuggestionsMenu();
+
+    return (
+      <InputGroup
+        className={classNames('suggestible-input', className)}
+        value={value as string}
+        defaultValue={defaultValue as string}
+        onChange={(e: any) => {
+          onValueChange(e.target.value);
+        }}
+        rightElement={
+          suggestionsMenu && (
+            <Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>
+              <Button icon={IconNames.CARET_DOWN} minimal />
+            </Popover>
+          )
+        }
+        large={large}
+        {...rest}
+      />
+    );
+  }
+}
diff --git a/web-console/src/components/table-cell/table-cell.tsx b/web-console/src/components/table-cell/table-cell.tsx
index 92cfa39..e4164fd 100644
--- a/web-console/src/components/table-cell/table-cell.tsx
+++ b/web-console/src/components/table-cell/table-cell.tsx
@@ -16,17 +16,14 @@
  * limitations under the License.
  */
 
-import { Intent } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import copy from 'copy-to-clipboard';
 import React from 'react';
 
-import { AppToaster } from '../../singletons/toaster';
 import { ActionIcon } from '../action-icon/action-icon';
 
 import './table-cell.scss';
 
-export interface NullTableCellProps extends React.Props<any> {
+export interface NullTableCellProps {
   value?: any;
   timestamp?: boolean;
   unparseable?: boolean;
diff --git a/web-console/src/components/table-column-selector/table-column-selector.spec.tsx b/web-console/src/components/table-column-selector/table-column-selector.spec.tsx
index 1f1d7dd..2cc3c74 100644
--- a/web-console/src/components/table-column-selector/table-column-selector.spec.tsx
+++ b/web-console/src/components/table-column-selector/table-column-selector.spec.tsx
@@ -26,7 +26,7 @@ describe('table column', () => {
     const tableColumn = (
       <TableColumnSelector
         columns={['a', 'b', 'c']}
-        onChange={(column: string) => {}}
+        onChange={() => {}}
         tableColumnsHidden={['a', 'b', 'c']}
       />
     );
diff --git a/web-console/src/components/table-column-selector/table-column-selector.tsx b/web-console/src/components/table-column-selector/table-column-selector.tsx
index 46660c2..2876404 100644
--- a/web-console/src/components/table-column-selector/table-column-selector.tsx
+++ b/web-console/src/components/table-column-selector/table-column-selector.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, Checkbox, FormGroup, Menu, Popover, Position } from '@blueprintjs/core';
+import { Button, Menu, Popover, Position } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
@@ -24,7 +24,7 @@ import { MenuCheckbox } from '../menu-checkbox/menu-checkbox';
 
 import './table-column-selector.scss';
 
-interface TableColumnSelectorProps extends React.Props<any> {
+interface TableColumnSelectorProps {
   columns: string[];
   onChange: (column: string) => void;
   tableColumnsHidden: string[];
diff --git a/web-console/src/components/timed-button/timed-button.scss b/web-console/src/components/timed-button/timed-button.scss
index 2879880..f4d7700 100644
--- a/web-console/src/components/timed-button/timed-button.scss
+++ b/web-console/src/components/timed-button/timed-button.scss
@@ -17,6 +17,5 @@
  */
 
 .timed-button {
-    padding: 10px 10px 5px 10px;
-  }
-
+  padding: 10px 10px 5px 10px;
+}
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 85e0f44..8485a07 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -41,7 +41,9 @@ import {
 
 import './console-application.scss';
 
-export interface ConsoleApplicationProps extends React.Props<any> {
+type Capabilities = 'working-with-sql' | 'working-without-sql' | 'broken';
+
+export interface ConsoleApplicationProps {
   hideLegacy: boolean;
   baseURL?: string;
   customHeaderName?: string;
@@ -60,11 +62,9 @@ export class ConsoleApplication extends React.PureComponent<
 > {
   static MESSAGE_KEY = 'druid-console-message';
   static MESSAGE_DISMISSED = 'dismissed';
-  private capabilitiesQueryManager: QueryManager<string, string>;
+  private capabilitiesQueryManager: QueryManager<string, Capabilities>;
 
-  static async discoverCapabilities(): Promise<
-    'working-with-sql' | 'working-without-sql' | 'broken'
-  > {
+  static async discoverCapabilities(): Promise<Capabilities> {
     try {
       await axios.post('/druid/v2/sql', { query: 'SELECT 1337' });
     } catch (e) {
@@ -109,13 +109,13 @@ export class ConsoleApplication extends React.PureComponent<
     });
   }
 
-  private supervisorId: string | null;
-  private taskId: string | null;
-  private openDialog: string | null;
-  private datasource: string | null;
-  private onlyUnavailable: boolean | null;
-  private initQuery: string | null;
-  private middleManager: string | null;
+  private supervisorId: string | undefined;
+  private taskId: string | undefined;
+  private openDialog: string | undefined;
+  private datasource: string | undefined;
+  private onlyUnavailable: boolean | undefined;
+  private initQuery: string | undefined;
+  private middleManager: string | undefined;
 
   constructor(props: ConsoleApplicationProps, context: any) {
     super(props, context);
@@ -134,16 +134,16 @@ export class ConsoleApplication extends React.PureComponent<
     }
 
     this.capabilitiesQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async () => {
         const capabilities = await ConsoleApplication.discoverCapabilities();
         if (capabilities !== 'working-with-sql') {
           ConsoleApplication.shownNotifications(capabilities);
         }
         return capabilities;
       },
-      onStateChange: ({ result, loading, error }) => {
+      onStateChange: ({ result, loading }) => {
         this.setState({
-          noSqlMode: result === 'working-with-sql' ? false : true,
+          noSqlMode: result !== 'working-with-sql',
           capabilitiesLoading: loading,
         });
       },
@@ -160,13 +160,13 @@ export class ConsoleApplication extends React.PureComponent<
 
   private resetInitialsWithDelay() {
     setTimeout(() => {
-      this.taskId = null;
-      this.supervisorId = null;
-      this.openDialog = null;
-      this.datasource = null;
-      this.onlyUnavailable = null;
-      this.initQuery = null;
-      this.middleManager = null;
+      this.taskId = undefined;
+      this.supervisorId = undefined;
+      this.openDialog = undefined;
+      this.datasource = undefined;
+      this.onlyUnavailable = undefined;
+      this.initQuery = undefined;
+      this.middleManager = undefined;
     }, 50);
   }
 
@@ -177,7 +177,7 @@ export class ConsoleApplication extends React.PureComponent<
     this.resetInitialsWithDelay();
   };
 
-  private goToTask = (taskId: string | null, openDialog?: string) => {
+  private goToTask = (taskId: string | undefined, openDialog?: string) => {
     this.taskId = taskId;
     if (openDialog) this.openDialog = openDialog;
     window.location.hash = 'tasks';
diff --git a/web-console/src/dialogs/about-dialog/about-dialog.spec.tsx b/web-console/src/dialogs/about-dialog/about-dialog.spec.tsx
index fcc3d85..e6ac855 100644
--- a/web-console/src/dialogs/about-dialog/about-dialog.spec.tsx
+++ b/web-console/src/dialogs/about-dialog/about-dialog.spec.tsx
@@ -24,7 +24,7 @@ import { AboutDialog } from './about-dialog';
 describe('about dialog', () => {
   it('matches snapshot', () => {
     const aboutDialog = <AboutDialog onClose={() => null} />;
-    const { container } = render(aboutDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(aboutDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/about-dialog/about-dialog.tsx b/web-console/src/dialogs/about-dialog/about-dialog.tsx
index be0a89f..fe2ea9d 100644
--- a/web-console/src/dialogs/about-dialog/about-dialog.tsx
+++ b/web-console/src/dialogs/about-dialog/about-dialog.tsx
@@ -28,7 +28,7 @@ import {
   DRUID_WEBSITE,
 } from '../../variables';
 
-export interface AboutDialogProps extends React.Props<any> {
+export interface AboutDialogProps {
   onClose: () => void;
 }
 
diff --git a/web-console/src/dialogs/async-action-dialog/async-action-dialog.spec.tsx b/web-console/src/dialogs/async-action-dialog/async-action-dialog.spec.tsx
index d54a75a..27b468f 100644
--- a/web-console/src/dialogs/async-action-dialog/async-action-dialog.spec.tsx
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.spec.tsx
@@ -28,13 +28,13 @@ describe('async action dialog', () => {
         action={() => {
           return Promise.resolve();
         }}
-        onClose={(success: boolean) => null}
+        onClose={() => null}
         confirmButtonText={'test'}
         successText={'test'}
         failText={'test'}
       />
     );
-    const { container } = render(asyncActionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(asyncActionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
index c5b11cd..e5a26c3 100644
--- a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
@@ -16,25 +16,14 @@
  * limitations under the License.
  */
 
-import {
-  Button,
-  ButtonGroup,
-  Classes,
-  Dialog,
-  FormGroup,
-  Icon,
-  Intent,
-  NumericInput,
-  ProgressBar,
-  TagInput,
-} from '@blueprintjs/core';
+import { Button, Classes, Dialog, Icon, Intent, ProgressBar } from '@blueprintjs/core';
 import { IconName } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import React from 'react';
 
 import { AppToaster } from '../../singletons/toaster';
 
-export interface AsyncAlertDialogProps extends React.Props<any> {
+export interface AsyncAlertDialogProps {
   action: null | (() => Promise<void>);
   onClose: (success: boolean) => void;
   confirmButtonText: string;
diff --git a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
index 5e8951c..749de70 100644
--- a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
@@ -240,7 +240,7 @@ exports[`compaction dialog matches snapshot 1`] = `
               class="bp3-form-content"
             >
               <div
-                class="bp3-input-group"
+                class="bp3-input-group suggestible-input"
               >
                 <input
                   class="bp3-input"
diff --git a/web-console/src/dialogs/compaction-dialog/compaction-dialog.spec.tsx b/web-console/src/dialogs/compaction-dialog/compaction-dialog.spec.tsx
index 7cd5a4c..38be490 100644
--- a/web-console/src/dialogs/compaction-dialog/compaction-dialog.spec.tsx
+++ b/web-console/src/dialogs/compaction-dialog/compaction-dialog.spec.tsx
@@ -26,13 +26,13 @@ describe('compaction dialog', () => {
     const compactionDialog = (
       <CompactionDialog
         onClose={() => null}
-        onSave={(config: any) => null}
+        onSave={() => null}
         onDelete={() => null}
         datasource={'test'}
         configData={'test'}
       />
     );
-    const { container } = render(compactionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(compactionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx b/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx
index 72484b2..f5d9508 100644
--- a/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx
+++ b/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx
@@ -23,7 +23,7 @@ import { AutoForm } from '../../components';
 
 import './compaction-dialog.scss';
 
-export interface CompactionDialogProps extends React.Props<any> {
+export interface CompactionDialogProps {
   onClose: () => void;
   onSave: (config: any) => void;
   onDelete: () => void;
diff --git a/web-console/src/dialogs/coordinator-dynamic-config/__snapshots__/coordinator-dynamic-config.spec.tsx.snap b/web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap
similarity index 92%
copy from web-console/src/dialogs/coordinator-dynamic-config/__snapshots__/coordinator-dynamic-config.spec.tsx.snap
copy to web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap
index bccc553..b8cb588 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config/__snapshots__/coordinator-dynamic-config.spec.tsx.snap
+++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap
@@ -16,7 +16,7 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
       tabindex="0"
     >
       <div
-        class="bp3-dialog snitch-dialog coordinator-dynamic-config"
+        class="bp3-dialog snitch-dialog coordinator-dynamic-config-dialog"
       >
         <div
           class="bp3-dialog-header"
@@ -77,16 +77,6 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
             class="bp3-dialog-footer-actions"
           >
             <button
-              class="bp3-button bp3-minimal left-align-button"
-              type="button"
-            >
-              <span
-                class="bp3-button-text"
-              >
-                History
-              </span>
-            </button>
-            <button
               class="bp3-button bp3-intent-primary"
               type="button"
             >
diff --git a/web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.scss b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.scss
similarity index 96%
rename from web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.scss
rename to web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.scss
index 8032103..0213e2c 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.scss
+++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.scss
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-.coordinator-dynamic-config {
+.coordinator-dynamic-config-dialog {
   &.bp3-dialog {
     margin-top: 5vh;
     top: 5%;
diff --git a/web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.spec.tsx b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.spec.tsx
similarity index 87%
rename from web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.spec.tsx
rename to web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.spec.tsx
index fe99155..660b027 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.spec.tsx
+++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.spec.tsx
@@ -19,12 +19,12 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { CoordinatorDynamicConfigDialog } from './coordinator-dynamic-config';
+import { CoordinatorDynamicConfigDialog } from './coordinator-dynamic-config-dialog';
 
 describe('coordinator dynamic config', () => {
   it('matches snapshot', () => {
     const coordinatorDynamicConfig = <CoordinatorDynamicConfigDialog onClose={() => null} />;
-    const { container } = render(coordinatorDynamicConfig, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(coordinatorDynamicConfig);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.tsx b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx
similarity index 93%
rename from web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.tsx
rename to web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx
index ca8bd8f..233d532 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config/coordinator-dynamic-config.tsx
+++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx
@@ -26,9 +26,9 @@ import { AppToaster } from '../../singletons/toaster';
 import { getDruidErrorMessage, QueryManager } from '../../utils';
 import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
 
-import './coordinator-dynamic-config.scss';
+import './coordinator-dynamic-config-dialog.scss';
 
-export interface CoordinatorDynamicConfigDialogProps extends React.Props<any> {
+export interface CoordinatorDynamicConfigDialogProps {
   onClose: () => void;
 }
 
@@ -41,7 +41,7 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
   CoordinatorDynamicConfigDialogProps,
   CoordinatorDynamicConfigDialogState
 > {
-  private historyQueryManager: QueryManager<string, any>;
+  private historyQueryManager: QueryManager<null, any>;
 
   constructor(props: CoordinatorDynamicConfigDialogProps) {
     super(props);
@@ -49,24 +49,24 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
       dynamicConfig: null,
       historyRecords: [],
     };
-  }
-
-  componentDidMount() {
-    this.getClusterConfig();
 
     this.historyQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async () => {
         const historyResp = await axios(`/druid/coordinator/v1/config/history?count=100`);
         return historyResp.data;
       },
-      onStateChange: ({ result, loading, error }) => {
+      onStateChange: ({ result }) => {
         this.setState({
           historyRecords: result,
         });
       },
     });
+  }
+
+  componentDidMount() {
+    this.getClusterConfig();
 
-    this.historyQueryManager.runQuery(`dummy`);
+    this.historyQueryManager.runQuery(null);
   }
 
   async getClusterConfig() {
@@ -118,7 +118,7 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
 
     return (
       <SnitchDialog
-        className="coordinator-dynamic-config"
+        className="coordinator-dynamic-config-dialog"
         isOpen
         onSave={this.saveClusterConfig}
         onClose={onClose}
diff --git a/web-console/src/dialogs/history-dialog/history-dialog.spec.tsx b/web-console/src/dialogs/history-dialog/history-dialog.spec.tsx
index 5a26884..7281186 100644
--- a/web-console/src/dialogs/history-dialog/history-dialog.spec.tsx
+++ b/web-console/src/dialogs/history-dialog/history-dialog.spec.tsx
@@ -32,7 +32,7 @@ describe('history dialog', () => {
         isOpen
       />
     );
-    const { container } = render(historyDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(historyDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/history-dialog/history-dialog.tsx b/web-console/src/dialogs/history-dialog/history-dialog.tsx
index 45ebfc1..c07c620 100644
--- a/web-console/src/dialogs/history-dialog/history-dialog.tsx
+++ b/web-console/src/dialogs/history-dialog/history-dialog.tsx
@@ -24,7 +24,7 @@ import { JSONCollapse } from '../../components';
 import './history-dialog.scss';
 
 interface HistoryDialogProps extends IDialogProps {
-  historyRecords: any;
+  historyRecords: any[];
 }
 
 interface HistoryDialogState {}
@@ -45,13 +45,13 @@ export class HistoryDialog extends React.PureComponent<HistoryDialogProps, Histo
         <>
           <span className="history-dialog-title">History</span>
           <div className="history-record-entries">
-            {historyRecords.map((record: any) => {
+            {historyRecords.map((record, i) => {
               const auditInfo = record.auditInfo;
               const auditTime = record.auditTime;
               const formattedTime = auditTime.replace('T', ' ').substring(0, auditTime.length - 5);
 
               return (
-                <div key={record.auditTime} className="history-record-entry">
+                <div key={i} className="history-record-entry">
                   <Card>
                     <div className="history-record-title">
                       <span className="history-record-title-change">Change</span>
diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts
index 8a4ebe9..27bd358 100644
--- a/web-console/src/dialogs/index.ts
+++ b/web-console/src/dialogs/index.ts
@@ -18,10 +18,10 @@
 export * from './about-dialog/about-dialog';
 export * from './async-action-dialog/async-action-dialog';
 export * from './compaction-dialog/compaction-dialog';
-export * from './coordinator-dynamic-config/coordinator-dynamic-config';
+export * from './coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
 export * from './history-dialog/history-dialog';
 export * from './lookup-edit-dialog/lookup-edit-dialog';
-export * from './overlord-dynamic-config/overlord-dynamic-config';
+export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
 export * from './query-plan-dialog/query-plan-dialog';
 export * from './retention-dialog/retention-dialog';
 export * from './snitch-dialog/snitch-dialog';
diff --git a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx
index 214cf49..8411612 100644
--- a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx
+++ b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx
@@ -38,7 +38,7 @@ describe('lookup edit dialog', () => {
       />
     );
 
-    const { container } = render(lookupEditDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(lookupEditDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx
index 9415e60..180ec2b 100644
--- a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx
+++ b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx
@@ -32,7 +32,7 @@ import { validJson } from '../../utils';
 
 import './lookup-edit-dialog.scss';
 
-export interface LookupEditDialogProps extends React.Props<any> {
+export interface LookupEditDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSubmit: () => void;
@@ -105,7 +105,6 @@ export class LookupEditDialog extends React.PureComponent<
       lookupVersion,
       onChange,
       isEdit,
-      allLookupTiers,
     } = this.props;
 
     const disableSubmit =
@@ -155,8 +154,6 @@ export class LookupEditDialog extends React.PureComponent<
           value={lookupSpec}
           editorProps={{ $blockScrolling: Infinity }}
           setOptions={{
-            enableBasicAutocompletion: false,
-            enableLiveAutocompletion: false,
             tabSize: 2,
           }}
           style={{}}
diff --git a/web-console/src/dialogs/coordinator-dynamic-config/__snapshots__/coordinator-dynamic-config.spec.tsx.snap b/web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap
similarity index 83%
rename from web-console/src/dialogs/coordinator-dynamic-config/__snapshots__/coordinator-dynamic-config.spec.tsx.snap
rename to web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap
index bccc553..fcde8c0 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config/__snapshots__/coordinator-dynamic-config.spec.tsx.snap
+++ b/web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`coordinator dynamic config matches snapshot 1`] = `
+exports[`overload dynamic config matches snapshot 1`] = `
 <div
   class="bp3-portal"
 >
@@ -16,7 +16,7 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
       tabindex="0"
     >
       <div
-        class="bp3-dialog snitch-dialog coordinator-dynamic-config"
+        class="bp3-dialog snitch-dialog overlord-dynamic-config-dialog"
       >
         <div
           class="bp3-dialog-header"
@@ -24,7 +24,7 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
           <h4
             class="bp3-heading"
           >
-            Coordinator dynamic config
+            Overlord dynamic config
           </h4>
           <button
             aria-label="Close"
@@ -56,10 +56,10 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
           class="bp3-dialog-body"
         >
           <p>
-            Edit the coordinator dynamic configuration on the fly. For more information please refer to the
+            Edit the overlord dynamic configuration on the fly. For more information please refer to the
              
             <a
-              href="https://druid.apache.org/docs/latest/configuration/index.html#dynamic-configuration"
+              href="https://druid.apache.org/docs/latest/configuration/index.html#overlord-dynamic-configuration"
               target="_blank"
             >
               documentation
@@ -77,16 +77,6 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
             class="bp3-dialog-footer-actions"
           >
             <button
-              class="bp3-button bp3-minimal left-align-button"
-              type="button"
-            >
-              <span
-                class="bp3-button-text"
-              >
-                History
-              </span>
-            </button>
-            <button
               class="bp3-button bp3-intent-primary"
               type="button"
             >
diff --git a/web-console/src/dialogs/about-dialog/about-dialog.spec.tsx b/web-console/src/dialogs/overlord-dynamic-config-dialog/overload-dynamic-config-dialog.spec.tsx
similarity index 75%
copy from web-console/src/dialogs/about-dialog/about-dialog.spec.tsx
copy to web-console/src/dialogs/overlord-dynamic-config-dialog/overload-dynamic-config-dialog.spec.tsx
index fcc3d85..8a2011d 100644
--- a/web-console/src/dialogs/about-dialog/about-dialog.spec.tsx
+++ b/web-console/src/dialogs/overlord-dynamic-config-dialog/overload-dynamic-config-dialog.spec.tsx
@@ -19,12 +19,13 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { AboutDialog } from './about-dialog';
+import { OverlordDynamicConfigDialog } from './overlord-dynamic-config-dialog';
 
-describe('about dialog', () => {
+describe('overload dynamic config', () => {
   it('matches snapshot', () => {
-    const aboutDialog = <AboutDialog onClose={() => null} />;
-    const { container } = render(aboutDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    const lookupEditDialog = <OverlordDynamicConfigDialog onClose={() => null} />;
+
+    render(lookupEditDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/overlord-dynamic-config/overlord-dynamic-config.scss b/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.scss
similarity index 97%
rename from web-console/src/dialogs/overlord-dynamic-config/overlord-dynamic-config.scss
rename to web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.scss
index 6e676ad..f655cc9 100644
--- a/web-console/src/dialogs/overlord-dynamic-config/overlord-dynamic-config.scss
+++ b/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.scss
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-.overlord-dynamic-config {
+.overlord-dynamic-config-dialog {
   &.bp3-dialog {
     margin-top: 5vh;
     top: 5%;
diff --git a/web-console/src/dialogs/overlord-dynamic-config/overlord-dynamic-config.tsx b/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx
similarity index 94%
rename from web-console/src/dialogs/overlord-dynamic-config/overlord-dynamic-config.tsx
rename to web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx
index 81ee2f6..dcf9423 100644
--- a/web-console/src/dialogs/overlord-dynamic-config/overlord-dynamic-config.tsx
+++ b/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx
@@ -26,9 +26,9 @@ import { AppToaster } from '../../singletons/toaster';
 import { getDruidErrorMessage, QueryManager } from '../../utils';
 import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
 
-import './overlord-dynamic-config.scss';
+import './overlord-dynamic-config-dialog.scss';
 
-export interface OverlordDynamicConfigDialogProps extends React.Props<any> {
+export interface OverlordDynamicConfigDialogProps {
   onClose: () => void;
 }
 
@@ -51,22 +51,22 @@ export class OverlordDynamicConfigDialog extends React.PureComponent<
       allJSONValid: true,
       historyRecords: [],
     };
-  }
-
-  componentDidMount() {
-    this.getConfig();
 
     this.historyQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async () => {
         const historyResp = await axios(`/druid/indexer/v1/worker/history?count=100`);
         return historyResp.data;
       },
-      onStateChange: ({ result, loading, error }) => {
+      onStateChange: ({ result }) => {
         this.setState({
           historyRecords: result,
         });
       },
     });
+  }
+
+  componentDidMount() {
+    this.getConfig();
 
     this.historyQueryManager.runQuery(`dummy`);
   }
@@ -120,7 +120,7 @@ export class OverlordDynamicConfigDialog extends React.PureComponent<
 
     return (
       <SnitchDialog
-        className="overlord-dynamic-config"
+        className="overlord-dynamic-config-dialog"
         isOpen
         onSave={this.saveConfig}
         onClose={onClose}
diff --git a/web-console/src/dialogs/overlord-dynamic-config/__snapshots__/overload-dynamic-config.spec.tsx.snap b/web-console/src/dialogs/overlord-dynamic-config/__snapshots__/overload-dynamic-config.spec.tsx.snap
deleted file mode 100644
index 1111228..0000000
--- a/web-console/src/dialogs/overlord-dynamic-config/__snapshots__/overload-dynamic-config.spec.tsx.snap
+++ /dev/null
@@ -1,356 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`overload dynamic config matches snapshot 1`] = `
-<div
-  class="bp3-portal"
->
-  <div
-    class="bp3-overlay bp3-overlay-open bp3-overlay-scroll-container"
-  >
-    <div
-      class="bp3-overlay-backdrop bp3-overlay-appear bp3-overlay-appear-active"
-      tabindex="0"
-    />
-    <div
-      class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
-      tabindex="0"
-    >
-      <div
-        class="bp3-dialog lookup-edit-dialog"
-      >
-        <div
-          class="bp3-dialog-header"
-        >
-          <h4
-            class="bp3-heading"
-          >
-            Add lookup
-          </h4>
-          <button
-            aria-label="Close"
-            class="bp3-button bp3-minimal bp3-dialog-close-button"
-            type="button"
-          >
-            <span
-              class="bp3-icon bp3-icon-small-cross"
-              icon="small-cross"
-            >
-              <svg
-                data-icon="small-cross"
-                height="20"
-                viewBox="0 0 20 20"
-                width="20"
-              >
-                <desc>
-                  small-cross
-                </desc>
-                <path
-                  d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L11.41 10z"
-                  fill-rule="evenodd"
-                />
-              </svg>
-            </span>
-          </button>
-        </div>
-        <div
-          class="bp3-form-group lookup-label"
-        >
-          <label
-            class="bp3-label"
-          >
-            Name: 
-             
-            <span
-              class="bp3-text-muted"
-            />
-          </label>
-          <div
-            class="bp3-form-content"
-          >
-            <div
-              class="bp3-input-group"
-            >
-              <input
-                class="bp3-input"
-                placeholder="Enter the lookup name"
-                style="padding-right: 10px;"
-                type="text"
-                value="test"
-              />
-            </div>
-          </div>
-        </div>
-        <div
-          class="bp3-form-group lookup-label"
-        >
-          <label
-            class="bp3-label"
-          >
-            Tier:
-             
-            <span
-              class="bp3-text-muted"
-            />
-          </label>
-          <div
-            class="bp3-form-content"
-          >
-            <div
-              class="bp3-html-select"
-            >
-              <select>
-                <option
-                  value="a"
-                >
-                  a
-                </option>
-                <option
-                  value="b"
-                >
-                  b
-                </option>
-                <option
-                  value="c"
-                >
-                  c
-                </option>
-                <option
-                  value="d"
-                >
-                  d
-                </option>
-                <option
-                  value="e"
-                >
-                  e
-                </option>
-                <option
-                  value="f"
-                >
-                  f
-                </option>
-                <option
-                  value="g"
-                >
-                  g
-                </option>
-                <option
-                  value="h"
-                >
-                  h
-                </option>
-                <option
-                  value="i"
-                >
-                  i
-                </option>
-                <option
-                  value="j"
-                >
-                  j
-                </option>
-              </select>
-              <span
-                class="bp3-icon bp3-icon-double-caret-vertical"
-                icon="double-caret-vertical"
-              >
-                <svg
-                  data-icon="double-caret-vertical"
-                  height="16"
-                  viewBox="0 0 16 16"
-                  width="16"
-                >
-                  <desc>
-                    double-caret-vertical
-                  </desc>
-                  <path
-                    d="M5 7h6a1.003 1.003 0 0 0 .71-1.71l-3-3C8.53 2.11 8.28 2 8 2s-.53.11-.71.29l-3 3A1.003 1.003 0 0 0 5 7zm6 2H5a1.003 1.003 0 0 0-.71 1.71l3 3c.18.18.43.29.71.29s.53-.11.71-.29l3-3A1.003 1.003 0 0 0 11 9z"
-                    fill-rule="evenodd"
-                  />
-                </svg>
-              </span>
-            </div>
-          </div>
-        </div>
-        <div
-          class="bp3-form-group lookup-label"
-        >
-          <label
-            class="bp3-label"
-          >
-            Version:
-             
-            <span
-              class="bp3-text-muted"
-            />
-          </label>
-          <div
-            class="bp3-form-content"
-          >
-            <div
-              class="bp3-input-group"
-            >
-              <input
-                class="bp3-input"
-                placeholder="Enter the lookup version"
-                style="padding-right: 0px;"
-                type="text"
-                value="test"
-              />
-              <span
-                class="bp3-input-action"
-              >
-                <button
-                  class="bp3-button bp3-minimal"
-                  type="button"
-                >
-                  <span
-                    class="bp3-button-text"
-                  >
-                    Use ISO as version
-                  </span>
-                </button>
-              </span>
-            </div>
-          </div>
-        </div>
-        <div
-          class="bp3-form-group lookup-label"
-        >
-          <label
-            class="bp3-label"
-          >
-            Spec:
-             
-            <span
-              class="bp3-text-muted"
-            />
-          </label>
-          <div
-            class="bp3-form-content"
-          />
-        </div>
-        <div
-          class=" ace_editor ace-tm lookup-edit-dialog-textarea"
-          id="brace-editor"
-          style="width: auto; height: 40vh;"
-        >
-          <textarea
-            autocapitalize="off"
-            autocorrect="off"
-            class="ace_text-input"
-            spellcheck="false"
-            style="opacity: 0;"
-            wrap="off"
-          />
-          <div
-            aria-hidden="true"
-            class="ace_gutter"
-            style="display: none;"
-          >
-            <div
-              class="ace_layer ace_gutter-layer ace_folding-enabled"
-            />
-            <div
-              class="ace_gutter-active-line"
-            />
-          </div>
-          <div
-            class="ace_scroller"
-          >
-            <div
-              class="ace_content"
-            >
-              <div
-                class="ace_layer ace_print-margin-layer"
-              >
-                <div
-                  class="ace_print-margin"
-                  style="left: 4px; visibility: hidden;"
-                />
-              </div>
-              <div
-                class="ace_layer ace_marker-layer"
-              />
-              <div
-                class="ace_layer ace_text-layer"
-                style="padding: 0px 4px;"
-              />
-              <div
-                class="ace_layer ace_marker-layer"
-              />
-              <div
-                class="ace_layer ace_cursor-layer ace_hidden-cursors"
-              >
-                <div
-                  class="ace_cursor"
-                />
-              </div>
-            </div>
-          </div>
-          <div
-            class="ace_scrollbar ace_scrollbar-v"
-            style="display: none; width: 20px;"
-          >
-            <div
-              class="ace_scrollbar-inner"
-              style="width: 20px;"
-            />
-          </div>
-          <div
-            class="ace_scrollbar ace_scrollbar-h"
-            style="display: none; height: 20px;"
-          >
-            <div
-              class="ace_scrollbar-inner"
-              style="height: 20px;"
-            />
-          </div>
-          <div
-            style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
-          >
-            <div
-              style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
-            />
-            <div
-              style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
-            >
-              XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-            </div>
-          </div>
-        </div>
-        <div
-          class="bp3-dialog-footer"
-        >
-          <div
-            class="bp3-dialog-footer-actions"
-          >
-            <button
-              class="bp3-button"
-              type="button"
-            >
-              <span
-                class="bp3-button-text"
-              >
-                Close
-              </span>
-            </button>
-            <button
-              class="bp3-button bp3-disabled bp3-intent-primary"
-              disabled=""
-              tabindex="-1"
-              type="button"
-            >
-              <span
-                class="bp3-button-text"
-              >
-                Submit
-              </span>
-            </button>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
diff --git a/web-console/src/dialogs/overlord-dynamic-config/overload-dynamic-config.spec.tsx b/web-console/src/dialogs/overlord-dynamic-config/overload-dynamic-config.spec.tsx
deleted file mode 100644
index f9d635b..0000000
--- a/web-console/src/dialogs/overlord-dynamic-config/overload-dynamic-config.spec.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react';
-import { render } from 'react-testing-library';
-
-import { LookupEditDialog } from '../lookup-edit-dialog/lookup-edit-dialog';
-
-describe('overload dynamic config', () => {
-  it('matches snapshot', () => {
-    const lookupEditDialog = (
-      <LookupEditDialog
-        isOpen
-        onClose={() => null}
-        onSubmit={() => null}
-        onChange={() => null}
-        lookupName={'test'}
-        lookupTier={'test'}
-        lookupVersion={'test'}
-        lookupSpec={'test'}
-        isEdit={false}
-        allLookupTiers={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']}
-      />
-    );
-    const { container } = render(lookupEditDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
-  });
-});
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 19cdaf1..045b5d1 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
@@ -30,7 +30,7 @@ describe('query plan dialog', () => {
         onClose={() => null}
       />
     );
-    const { container } = render(queryPlanDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    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 c398d5b..8fa438b 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
@@ -23,7 +23,7 @@ import { BasicQueryExplanation, SemiJoinQueryExplanation } from '../../utils';
 
 import './query-plan-dialog.scss';
 
-export interface QueryPlanDialogProps extends React.Props<any> {
+export interface QueryPlanDialogProps {
   explainResult: BasicQueryExplanation | SemiJoinQueryExplanation | string | null;
   explainError: Error | null;
   onClose: () => void;
diff --git a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
index 769a0b2..554dcda 100644
--- a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
@@ -117,16 +117,6 @@ exports[`retention dialog matches snapshot 1`] = `
             class="bp3-dialog-footer-actions"
           >
             <button
-              class="bp3-button bp3-minimal left-align-button"
-              type="button"
-            >
-              <span
-                class="bp3-button-text"
-              >
-                History
-              </span>
-            </button>
-            <button
               class="bp3-button"
               type="button"
             >
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.array.spec.ts b/web-console/src/dialogs/retention-dialog/retention-dialog.array.spec.ts
deleted file mode 100644
index a496d40..0000000
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.array.spec.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react';
-
-import { reorderArray } from './retention-dialog';
-
-describe('reorderArray', () => {
-  it('works when nothing changes', () => {
-    const array = ['a', 'b', 'c', 'd', 'e'];
-
-    const newArray = reorderArray(array, 0, 0);
-
-    expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
-    expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
-  });
-
-  it('works upward', () => {
-    const array = ['a', 'b', 'c', 'd', 'e'];
-
-    let newArray = reorderArray(array, 2, 1);
-    expect(newArray).toEqual(['a', 'c', 'b', 'd', 'e']);
-    expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
-
-    newArray = reorderArray(array, 2, 0);
-    expect(newArray).toEqual(['c', 'a', 'b', 'd', 'e']);
-    expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
-  });
-
-  it('works downward', () => {
-    const array = ['a', 'b', 'c', 'd', 'e'];
-
-    let newArray = reorderArray(array, 2, 3);
-    expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
-    expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
-
-    newArray = reorderArray(array, 2, 4);
-    expect(newArray).toEqual(['a', 'b', 'd', 'c', 'e']);
-    expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
-  });
-});
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx
index bcf1e77..80ccb71 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { RetentionDialog } from './retention-dialog';
+import { reorderArray, RetentionDialog } from './retention-dialog';
 
 describe('retention dialog', () => {
   it('matches snapshot', () => {
@@ -33,7 +33,42 @@ describe('retention dialog', () => {
         onSave={() => null}
       />
     );
-    const { container } = render(retentionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(retentionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
+  });
+
+  describe('reorderArray', () => {
+    it('works when nothing changes', () => {
+      const array = ['a', 'b', 'c', 'd', 'e'];
+
+      const newArray = reorderArray(array, 0, 0);
+
+      expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
+      expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
+    });
+
+    it('works upward', () => {
+      const array = ['a', 'b', 'c', 'd', 'e'];
+
+      let newArray = reorderArray(array, 2, 1);
+      expect(newArray).toEqual(['a', 'c', 'b', 'd', 'e']);
+      expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
+
+      newArray = reorderArray(array, 2, 0);
+      expect(newArray).toEqual(['c', 'a', 'b', 'd', 'e']);
+      expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
+    });
+
+    it('works downward', () => {
+      const array = ['a', 'b', 'c', 'd', 'e'];
+
+      let newArray = reorderArray(array, 2, 3);
+      expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
+      expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
+
+      newArray = reorderArray(array, 2, 4);
+      expect(newArray).toEqual(['a', 'b', 'd', 'c', 'e']);
+      expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
+    });
   });
 });
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
index 1c7cfc8..f75a3d1 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
@@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import React from 'react';
 
-import { Rule, RuleEditor } from '../../components';
+import { RuleEditor } from '../../components';
 import { QueryManager } from '../../utils';
 import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
 
@@ -37,7 +37,7 @@ export function reorderArray<T>(items: T[], oldIndex: number, newIndex: number):
   return newItems;
 }
 
-export interface RetentionDialogProps extends React.Props<any> {
+export interface RetentionDialogProps {
   datasource: string;
   rules: any[];
   tiers: string[];
@@ -64,23 +64,23 @@ export class RetentionDialog extends React.PureComponent<
       currentRules: props.rules,
       historyRecords: [],
     };
-  }
 
-  componentDidMount() {
-    const { datasource } = this.props;
     this.historyQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async datasource => {
         const historyResp = await axios(`/druid/coordinator/v1/rules/${datasource}/history`);
         return historyResp.data;
       },
-      onStateChange: ({ result, loading, error }) => {
+      onStateChange: ({ result }) => {
         this.setState({
           historyRecords: result,
         });
       },
     });
+  }
 
-    this.historyQueryManager.runQuery(`dummy`);
+  componentDidMount() {
+    const { datasource } = this.props;
+    this.historyQueryManager.runQuery(datasource);
   }
 
   private save = (comment: string) => {
@@ -106,7 +106,7 @@ export class RetentionDialog extends React.PureComponent<
   onDeleteRule = (index: number) => {
     const { currentRules } = this.state;
 
-    const newRules = (currentRules || []).filter((r, i) => i !== index);
+    const newRules = (currentRules || []).filter((_r, i) => i !== index);
 
     this.setState({
       currentRules: newRules,
diff --git a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
index 6e695a9..5cf64b7 100644
--- a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
@@ -148,7 +148,18 @@ exports[`task table action dialog matches snapshot 1`] = `
         >
           <div
             class="footer-actions-left"
-          />
+          >
+            <button
+              class="bp3-button"
+              type="button"
+            >
+              <span
+                class="bp3-button-text"
+              >
+                test
+              </span>
+            </button>
+          </div>
           <div
             class="bp3-dialog-footer-actions"
           >
diff --git a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx
index 52decd2..00bf06b 100644
--- a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx
+++ b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx
@@ -21,19 +21,18 @@ import { render } from 'react-testing-library';
 
 import { SegmentTableActionDialog } from './segment-table-action-dialog';
 
-const basicAction = { title: 'test', onAction: () => null };
 describe('task table action dialog', () => {
   it('matches snapshot', () => {
     const taskTableActionDialog = (
       <SegmentTableActionDialog
         dataSourceId="test"
         segmentId="test"
-        actions={[]}
+        actions={[{ title: 'test', onAction: () => null }]}
         onClose={() => null}
         isOpen
       />
     );
-    const { container } = render(taskTableActionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(taskTableActionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
index 1ae592b..3dbfef8 100644
--- a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
+++ b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { IDialogProps, TextArea } from '@blueprintjs/core';
+import { IDialogProps } from '@blueprintjs/core';
 import React from 'react';
 
 import { ShowJson } from '../../components';
diff --git a/web-console/src/dialogs/show-value-dialog/__snapshots__/show-value-dialog.spec.tsx.snap b/web-console/src/dialogs/show-value-dialog/__snapshots__/show-value-dialog.spec.tsx.snap
index 144f880..88a1d05 100644
--- a/web-console/src/dialogs/show-value-dialog/__snapshots__/show-value-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/show-value-dialog/__snapshots__/show-value-dialog.spec.tsx.snap
@@ -24,7 +24,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
           <h4
             class="bp3-heading"
           >
-            Show value
+            Full value
           </h4>
           <button
             aria-label="Close"
diff --git a/web-console/src/dialogs/show-value-dialog/show-value-dialog.scss b/web-console/src/dialogs/show-value-dialog/show-value-dialog.scss
index 1ef9e94..8527164 100644
--- a/web-console/src/dialogs/show-value-dialog/show-value-dialog.scss
+++ b/web-console/src/dialogs/show-value-dialog/show-value-dialog.scss
@@ -16,18 +16,17 @@
  * limitations under the License.
  */
 
-.show-value-dialog{
-  &.bp3-dialog{
+.show-value-dialog {
+  &.bp3-dialog {
     padding-bottom: 10px;
   }
 
-  .bp3-input{
+  .bp3-input {
     margin: 10px;
     height: 400px;
   }
 
-  .bp3-dialog-footer-actions{
+  .bp3-dialog-footer-actions {
     padding-right: 10px;
-
   }
 }
diff --git a/web-console/src/dialogs/show-value-dialog/show-value-dialog.spec.tsx b/web-console/src/dialogs/show-value-dialog/show-value-dialog.spec.tsx
index d8dc72d..2d41656 100644
--- a/web-console/src/dialogs/show-value-dialog/show-value-dialog.spec.tsx
+++ b/web-console/src/dialogs/show-value-dialog/show-value-dialog.spec.tsx
@@ -31,7 +31,7 @@ describe('clipboard dialog', () => {
         }
       />
     );
-    const { container } = render(compactionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(compactionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx b/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
index 4a92aa8..9aed8b0 100644
--- a/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
+++ b/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
@@ -15,16 +15,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Button, Classes, Dialog, IconName, Intent, TextArea } from '@blueprintjs/core';
+import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import copy = require('copy-to-clipboard');
+import copy from 'copy-to-clipboard';
 import React from 'react';
 
 import { AppToaster } from '../../singletons/toaster';
 
 import './show-value-dialog.scss';
 
-export interface ShowValueDialogProps extends React.Props<any> {
+export interface ShowValueDialogProps {
   onClose: () => void;
   str: string;
 }
@@ -39,23 +39,22 @@ export class ShowValueDialog extends React.PureComponent<ShowValueDialogProps> {
     const { onClose, str } = this.props;
 
     return (
-      <Dialog className="show-value-dialog" isOpen onClose={onClose} title={'Show value'}>
+      <Dialog className="show-value-dialog" isOpen onClose={onClose} title="Full value">
         <TextArea value={str} />
         <div className={Classes.DIALOG_FOOTER_ACTIONS}>
-          <Button
-            icon={IconNames.DUPLICATE}
-            text={'Copy'}
-            onClick={() => {
-              copy(str, { format: 'text/plain' });
-              AppToaster.show({
-                message: 'Value copied to clipboard',
-                intent: Intent.SUCCESS,
-              });
-            }}
-          />
-          <Button text={'Close'} intent={'primary'} onClick={onClose} />
+          <Button icon={IconNames.DUPLICATE} text={'Copy'} onClick={this.handleCopy} />
+          <Button text={'Close'} intent={Intent.PRIMARY} onClick={onClose} />
         </div>
       </Dialog>
     );
   }
+
+  private handleCopy = () => {
+    const { str } = this.props;
+    copy(str, { format: 'text/plain' });
+    AppToaster.show({
+      message: 'Value copied to clipboard',
+      intent: Intent.SUCCESS,
+    });
+  };
 }
diff --git a/web-console/src/dialogs/snitch-dialog/snitch-dialog.spec.tsx b/web-console/src/dialogs/snitch-dialog/snitch-dialog.spec.tsx
index 09d073e..3ecfbf7 100644
--- a/web-console/src/dialogs/snitch-dialog/snitch-dialog.spec.tsx
+++ b/web-console/src/dialogs/snitch-dialog/snitch-dialog.spec.tsx
@@ -24,7 +24,7 @@ import { SnitchDialog } from './snitch-dialog';
 describe('snitch dialog', () => {
   it('matches snapshot', () => {
     const snitchDialog = <SnitchDialog onSave={() => null} isOpen />;
-    const { container } = render(snitchDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(snitchDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx b/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx
index 17f813d..742ad55 100644
--- a/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx
+++ b/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx
@@ -44,7 +44,6 @@ export interface SnitchDialogState {
   comment: string;
 
   showFinalStep?: boolean;
-  saveDisabled?: boolean;
 
   showHistory?: boolean;
 }
@@ -55,7 +54,6 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
 
     this.state = {
       comment: '',
-      saveDisabled: true,
     };
   }
 
@@ -68,11 +66,8 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
   };
 
   changeComment(newComment: string) {
-    const { comment } = this.state;
-
     this.setState({
       comment: newComment,
-      saveDisabled: !newComment,
     });
   }
 
@@ -95,15 +90,14 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
     });
   };
 
-  goToHistory = () => {
+  handleGoToHistory = () => {
     this.setState({
       showHistory: true,
     });
   };
 
   renderFinalStep() {
-    const { onClose, children } = this.props;
-    const { saveDisabled, comment } = this.state;
+    const { comment } = this.state;
 
     return (
       <Dialog {...this.props}>
@@ -117,14 +111,14 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
             />
           </FormGroup>
         </div>
-
-        <div className={Classes.DIALOG_FOOTER}>{this.renderActions(saveDisabled)}</div>
+        <div className={Classes.DIALOG_FOOTER}>{this.renderActions(!comment)}</div>
       </Dialog>
     );
   }
 
   renderHistoryDialog() {
     const { historyRecords } = this.props;
+    if (!historyRecords) return;
     return (
       <HistoryDialog {...this.props} className="history-dialog" historyRecords={historyRecords}>
         <div className={Classes.DIALOG_FOOTER_ACTIONS}>
@@ -142,8 +136,13 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
 
     return (
       <div className={Classes.DIALOG_FOOTER_ACTIONS}>
-        {showFinalStep || historyRecords === undefined ? null : (
-          <Button className="left-align-button" minimal text="History" onClick={this.goToHistory} />
+        {!showFinalStep && historyRecords && (
+          <Button
+            className="left-align-button"
+            minimal
+            text="History"
+            onClick={this.handleGoToHistory}
+          />
         )}
 
         {showFinalStep ? (
@@ -178,7 +177,7 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
   }
 
   render() {
-    const { onClose, className, children, saveDisabled } = this.props;
+    const { children, saveDisabled } = this.props;
     const { showFinalStep, showHistory } = this.state;
 
     if (showFinalStep) return this.renderFinalStep();
@@ -189,7 +188,6 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
     return (
       <Dialog isOpen {...propsClone}>
         <div className={Classes.DIALOG_BODY}>{children}</div>
-
         <div className={Classes.DIALOG_FOOTER}>{this.renderActions(saveDisabled)}</div>
       </Dialog>
     );
diff --git a/web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx b/web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx
index 35d7214..44e37d6 100644
--- a/web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx
+++ b/web-console/src/dialogs/spec-dialog/spec-dialog.spec.tsx
@@ -23,10 +23,8 @@ import { SpecDialog } from './spec-dialog';
 
 describe('spec dialog', () => {
   it('matches snapshot', () => {
-    const specDialog = (
-      <SpecDialog onSubmit={(spec: JSON) => null} onClose={() => null} title={'test'} />
-    );
-    const { container } = render(specDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    const specDialog = <SpecDialog onSubmit={() => null} onClose={() => null} title={'test'} />;
+    render(specDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/spec-dialog/spec-dialog.tsx b/web-console/src/dialogs/spec-dialog/spec-dialog.tsx
index 827d1ea..5cee087 100644
--- a/web-console/src/dialogs/spec-dialog/spec-dialog.tsx
+++ b/web-console/src/dialogs/spec-dialog/spec-dialog.tsx
@@ -22,7 +22,7 @@ import AceEditor from 'react-ace';
 
 import './spec-dialog.scss';
 
-export interface SpecDialogProps extends React.Props<any> {
+export interface SpecDialogProps {
   onSubmit: (spec: JSON) => void;
   onClose: () => void;
   title: string;
@@ -84,10 +84,7 @@ export class SpecDialog extends React.PureComponent<SpecDialogProps, SpecDialogS
           value={spec}
           width="100%"
           setOptions={{
-            enableBasicAutocompletion: true,
-            enableLiveAutocompletion: true,
             showLineNumbers: true,
-            enableSnippets: true,
             tabSize: 2,
           }}
           style={{}}
diff --git a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.spec.tsx b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.spec.tsx
index dfe54ee..d5fcdd4 100644
--- a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.spec.tsx
+++ b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.spec.tsx
@@ -32,7 +32,7 @@ describe('supervisor table action dialog', () => {
         isOpen
       />
     );
-    const { container } = render(supervisorTableActionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(supervisorTableActionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/table-action-dialog/table-action-dialog.scss b/web-console/src/dialogs/table-action-dialog/table-action-dialog.scss
index ca89d1c..4af0a88 100644
--- a/web-console/src/dialogs/table-action-dialog/table-action-dialog.scss
+++ b/web-console/src/dialogs/table-action-dialog/table-action-dialog.scss
@@ -75,7 +75,7 @@ $side-bar-width: 120px;
       left: $side-bar-width;
       right: 0;
       height: 100%;
-      padding: 10px 20px;
+      padding: 10px 20px 15px 20px;
     }
   }
 
diff --git a/web-console/src/dialogs/table-action-dialog/table-action-dialog.spec.tsx b/web-console/src/dialogs/table-action-dialog/table-action-dialog.spec.tsx
index 33b939b..1476924 100644
--- a/web-console/src/dialogs/table-action-dialog/table-action-dialog.spec.tsx
+++ b/web-console/src/dialogs/table-action-dialog/table-action-dialog.spec.tsx
@@ -30,7 +30,7 @@ describe('table action dialog', () => {
         isOpen
       />
     );
-    const { container } = render(tableActionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(tableActionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx b/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx
index 6795f97..b8cd596 100644
--- a/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx
+++ b/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx
@@ -47,11 +47,11 @@ export class TableActionDialog extends React.PureComponent<TableActionDialogProp
       <Dialog className="table-action-dialog" isOpen={isOpen} onClose={onClose} title={title}>
         <div className={Classes.DIALOG_BODY}>
           <div className="side-bar">
-            {sideButtonMetadata.map((d: SideButtonMetaData) => (
+            {sideButtonMetadata.map((d, i) => (
               <Button
                 className="tab-button"
                 icon={<Icon icon={d.icon} iconSize={20} />}
-                key={d.text}
+                key={i}
                 text={d.text}
                 intent={d.active ? Intent.PRIMARY : Intent.NONE}
                 minimal={!d.active}
diff --git a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.spec.tsx b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.spec.tsx
index 1b1d862..646b57d 100644
--- a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.spec.tsx
+++ b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.spec.tsx
@@ -33,7 +33,7 @@ describe('task table action dialog', () => {
         isOpen
       />
     );
-    const { container } = render(taskTableActionDialog, { container: document.body });
-    expect(container.firstChild).toMatchSnapshot();
+    render(taskTableActionDialog);
+    expect(document.body.lastChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 5480ef9..2e9aeac 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, HTMLSelect, InputGroup, Intent } from '@blueprintjs/core';
+import { Button, HTMLSelect, InputGroup } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import FileSaver from 'file-saver';
 import hasOwnProp from 'has-own-prop';
@@ -232,11 +232,6 @@ export function pluralIfNeeded(n: number, singular: string, plural?: string): st
   return `${formatNumber(n)} ${n === 1 ? singular : plural}`;
 }
 
-export function getHeadProp(results: Record<string, any>[], prop: string): any {
-  if (!results || !results.length) return null;
-  return results[0][prop] || null;
-}
-
 // ----------------------------
 
 export function parseJson(json: string): any {
@@ -274,10 +269,6 @@ export function parseStringToJSON(s: string): JSON | null {
   }
 }
 
-export function selectDefined<T, Q>(xs: (Q | null | undefined)[]): Q[] {
-  return xs.filter(Boolean) as any;
-}
-
 export function filterMap<T, Q>(xs: T[], f: (x: T, i?: number) => Q | null | undefined): Q[] {
   return (xs.map(f) as any).filter(Boolean);
 }
diff --git a/web-console/src/utils/ingestion-spec.tsx b/web-console/src/utils/ingestion-spec.tsx
index dc115cf..c31e12b 100644
--- a/web-console/src/utils/ingestion-spec.tsx
+++ b/web-console/src/utils/ingestion-spec.tsx
@@ -17,7 +17,6 @@
  */
 
 import { Code } from '@blueprintjs/core';
-import { number } from 'prop-types';
 import React from 'react';
 
 import { Field } from '../components/auto-form/auto-form';
@@ -184,11 +183,12 @@ export interface ParseSpec {
 
 export function hasParallelAbility(spec: IngestionSpec): boolean {
   const specType = getSpecType(spec);
-  return spec.type === 'index' || spec.type === 'index_parallel';
+  return specType === 'index' || specType === 'index_parallel';
 }
 
 export function isParallel(spec: IngestionSpec): boolean {
-  return spec.type === 'index_parallel';
+  const specType = getSpecType(spec);
+  return specType === 'index_parallel';
 }
 
 export type DimensionMode = 'specific' | 'auto-detect';
@@ -1483,6 +1483,45 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
     info: <>Used in determining when intermediate persists to disk should occur.</>,
   },
   {
+    name: 'indexSpec.bitmap.type',
+    label: 'Index bitmap type',
+    type: 'string',
+    defaultValue: 'concise',
+    suggestions: ['concise', 'roaring'],
+    info: <>Compression format for bitmap indexes.</>,
+  },
+  {
+    name: 'indexSpec.dimensionCompression',
+    label: 'Index dimension compression',
+    type: 'string',
+    defaultValue: 'lz4',
+    suggestions: ['lz4', 'lzf', 'uncompressed'],
+    info: <>Compression format for dimension columns.</>,
+  },
+  {
+    name: 'indexSpec.metricCompression',
+    label: 'Index metric compression',
+    type: 'string',
+    defaultValue: 'lz4',
+    suggestions: ['lz4', 'lzf', 'uncompressed'],
+    info: <>Compression format for metric columns.</>,
+  },
+  {
+    name: 'indexSpec.longEncoding',
+    label: 'Index long encoding',
+    type: 'string',
+    defaultValue: 'longs',
+    suggestions: ['longs', 'auto'],
+    info: (
+      <>
+        Encoding format for long-typed columns. Applies regardless of whether they are dimensions or
+        metrics. <Code>auto</Code> encodes the values using offset or lookup table depending on
+        column cardinality, and store them with variable size. <Code>longs</Code> stores the value
+        as-is with 8 bytes each.
+      </>
+    ),
+  },
+  {
     name: 'intermediatePersistPeriod',
     type: 'duration',
     defaultValue: 'PT10M',
@@ -1514,10 +1553,6 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
     ),
   },
   {
-    name: 'forceExtendableShardSpecs',
-    type: 'boolean',
-  },
-  {
     name: 'pushTimeout',
     type: 'number',
     defaultValue: 0,
diff --git a/web-console/src/utils/local-storage-backed-array.tsx b/web-console/src/utils/local-storage-backed-array.tsx
index daf227d..5383b4a 100644
--- a/web-console/src/utils/local-storage-backed-array.tsx
+++ b/web-console/src/utils/local-storage-backed-array.tsx
@@ -25,24 +25,21 @@ export class LocalStorageBackedArray<T> {
   constructor(key: LocalStorageKeys, array?: T[]) {
     this.key = key;
     if (!Array.isArray(array)) {
-      this.getDataFromStorage();
+      this.storedArray = this.getDataFromStorage();
     } else {
       this.storedArray = array;
       this.setDataInStorage();
     }
   }
 
-  private getDataFromStorage(): void {
-    let possibleArray: any;
+  private getDataFromStorage(): T[] {
     try {
-      possibleArray = JSON.parse(String(localStorageGet(this.key)));
+      const possibleArray: any = JSON.parse(String(localStorageGet(this.key)));
+      if (!Array.isArray(possibleArray)) return [];
+      return possibleArray;
     } catch {
-      // show all columns by default
-      possibleArray = [];
+      return [];
     }
-    if (!Array.isArray(possibleArray)) possibleArray = [];
-
-    this.storedArray = possibleArray;
   }
 
   private setDataInStorage(): void {
diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx
index cf70584..1aa50b7 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -32,6 +32,7 @@ export const LocalStorageKeys = {
   SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as 'segments-refresh-rate',
   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',
 };
 export type LocalStorageKeys = typeof LocalStorageKeys[keyof typeof LocalStorageKeys];
 
diff --git a/web-console/src/utils/query-manager.tsx b/web-console/src/utils/query-manager.tsx
index bf5e07a..8938067 100644
--- a/web-console/src/utils/query-manager.tsx
+++ b/web-console/src/utils/query-manager.tsx
@@ -36,8 +36,8 @@ export class QueryManager<Q, R> {
   private onStateChange?: (queryResolve: QueryStateInt<R>) => void;
 
   private terminated = false;
-  private nextQuery: Q;
-  private lastQuery: Q;
+  private nextQuery: Q | undefined;
+  private lastQuery: Q | undefined;
   private actuallyLoading = false;
   private state: QueryStateInt<R> = {
     result: null,
@@ -73,6 +73,7 @@ export class QueryManager<Q, R> {
 
   private run() {
     this.lastQuery = this.nextQuery;
+    if (typeof this.lastQuery === 'undefined') return;
     this.currentQueryId++;
     const myQueryId = this.currentQueryId;
 
@@ -134,7 +135,7 @@ export class QueryManager<Q, R> {
     }
   }
 
-  public getLastQuery(): Q {
+  public getLastQuery(): Q | undefined {
     return this.lastQuery;
   }
 
diff --git a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
index 64e34ba..dac1512 100755
--- a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
+++ b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
@@ -26,6 +26,7 @@ exports[`data source view matches snapshot 1`] = `
         Array [
           "Datasource",
           "Availability",
+          "Segment load/drop",
           "Retention",
           "Compaction",
           "Size",
@@ -112,6 +113,14 @@ exports[`data source view matches snapshot 1`] = `
         },
         Object {
           "Cell": [Function],
+          "Header": "Segment load/drop",
+          "accessor": "num_segments_to_load",
+          "filterable": false,
+          "id": "load-drop",
+          "show": true,
+        },
+        Object {
+          "Cell": [Function],
           "Header": "Retention",
           "accessor": [Function],
           "filterable": false,
diff --git a/web-console/src/views/datasource-view/datasource-view.spec.tsx b/web-console/src/views/datasource-view/datasource-view.spec.tsx
index cf78dbb..9bb0484 100644
--- a/web-console/src/views/datasource-view/datasource-view.spec.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.spec.tsx
@@ -24,11 +24,7 @@ import { DatasourcesView } from './datasource-view';
 describe('data source view', () => {
   it('matches snapshot', () => {
     const dataSourceView = shallow(
-      <DatasourcesView
-        goToQuery={(initSql: string) => {}}
-        goToSegments={(datasource: string, onlyUnavailable?: boolean) => {}}
-        noSqlMode={false}
-      />,
+      <DatasourcesView goToQuery={() => {}} goToSegments={() => {}} noSqlMode={false} />,
     );
     expect(dataSourceView).toMatchSnapshot();
   });
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx
index db14776..ab6f1b4 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -16,16 +16,7 @@
  * limitations under the License.
  */
 
-import {
-  Button,
-  FormGroup,
-  Icon,
-  InputGroup,
-  Intent,
-  Popover,
-  Position,
-  Switch,
-} from '@blueprintjs/core';
+import { Button, FormGroup, InputGroup, Intent, Switch } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import React from 'react';
@@ -55,12 +46,14 @@ import {
 } from '../../utils';
 import { BasicAction } from '../../utils/basic-action';
 import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
+import { deepGet } from '../../utils/object-change';
 
 import './datasource-view.scss';
 
 const tableColumns: string[] = [
   'Datasource',
   'Availability',
+  'Segment load/drop',
   'Retention',
   'Compaction',
   'Size',
@@ -70,13 +63,25 @@ const tableColumns: string[] = [
 const tableColumnsNoSql: string[] = [
   'Datasource',
   'Availability',
+  'Segment load/drop',
   'Retention',
   'Compaction',
   'Size',
   ActionCell.COLUMN_LABEL,
 ];
 
-export interface DatasourcesViewProps extends React.Props<any> {
+function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
+  const loadDrop: string[] = [];
+  if (segmentsToLoad) {
+    loadDrop.push(`${segmentsToLoad} segments to load`);
+  }
+  if (segmentsToDrop) {
+    loadDrop.push(`${segmentsToDrop} segments to drop`);
+  }
+  return loadDrop.join(', ') || 'No segments to load/drop';
+}
+
+export interface DatasourcesViewProps {
   goToQuery: (initSql: string) => void;
   goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
   noSqlMode: boolean;
@@ -90,10 +95,12 @@ interface Datasource {
 
 interface DatasourceQueryResultRow {
   datasource: string;
-  num_available_segments: number;
-  num_rows: number;
   num_segments: number;
+  num_available_segments: number;
+  num_segments_to_load: number;
+  num_segments_to_drop: number;
   size: number;
+  num_rows: number;
 }
 
 export interface DatasourcesViewState {
@@ -124,6 +131,17 @@ export class DatasourcesView extends React.PureComponent<
   static FULLY_AVAILABLE_COLOR = '#57d500';
   static PARTIALLY_AVAILABLE_COLOR = '#ffbf00';
 
+  static DATASOURCE_SQL = `SELECT
+  datasource,
+  COUNT(*) FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS num_segments,
+  COUNT(*) FILTER (WHERE is_available = 1 AND ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_available_segments,
+  COUNT(*) FILTER (WHERE is_published = 1 AND is_overshadowed = 0 AND is_available = 0) AS num_segments_to_load,
+  COUNT(*) FILTER (WHERE is_available = 1 AND NOT ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_segments_to_drop,
+  SUM("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS size,
+  SUM("num_rows") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS num_rows
+FROM sys.segments
+GROUP BY 1`;
+
   static formatRules(rules: any[]): string {
     if (rules.length === 0) {
       return 'No rules';
@@ -135,7 +153,7 @@ export class DatasourcesView extends React.PureComponent<
   }
 
   private datasourceQueryManager: QueryManager<
-    string,
+    boolean,
     { tiers: string[]; defaultRules: any[]; datasources: Datasource[] }
   >;
 
@@ -162,30 +180,31 @@ export class DatasourcesView extends React.PureComponent<
         LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
       ),
     };
-  }
-
-  componentDidMount(): void {
-    const { noSqlMode } = this.props;
-    const { hiddenColumns } = this.state;
 
     this.datasourceQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async noSqlMode => {
         let datasources: DatasourceQueryResultRow[];
         if (!noSqlMode) {
-          datasources = await queryDruidSql({ query });
+          datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL });
         } else {
           const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
           const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
           const loadstatus = loadstatusResp.data;
-          datasources = datasourcesResp.data.map((d: any) => {
-            return {
-              datasource: d.name,
-              num_available_segments: d.properties.segments.count,
-              size: d.properties.segments.size,
-              num_segments: d.properties.segments.count + loadstatus[d.name],
-              num_rows: -1,
-            };
-          });
+          datasources = datasourcesResp.data.map(
+            (d: any): DatasourceQueryResultRow => {
+              const segmentsToLoad = Number(loadstatus[d.name] || 0);
+              const availableSegments = Number(deepGet(d, 'properties.segments.count'));
+              return {
+                datasource: d.name,
+                num_available_segments: availableSegments,
+                num_segments: availableSegments + segmentsToLoad,
+                num_segments_to_load: segmentsToLoad,
+                num_segments_to_drop: 0,
+                size: d.properties.segments.size,
+                num_rows: -1,
+              };
+            },
+          );
         }
 
         const seen = countBy(datasources, (x: any) => x.datasource);
@@ -234,15 +253,11 @@ export class DatasourcesView extends React.PureComponent<
         });
       },
     });
+  }
 
-    this.datasourceQueryManager.runQuery(`SELECT
-  datasource,
-  COUNT(*) AS num_segments,
-  SUM(is_available) AS num_available_segments,
-  SUM("size") AS size,
-  SUM("num_rows") AS num_rows
-FROM sys.segments
-GROUP BY 1`);
+  componentDidMount(): void {
+    const { noSqlMode } = this.props;
+    this.datasourceQueryManager.runQuery(noSqlMode);
   }
 
   componentWillUnmount(): void {
@@ -581,7 +596,7 @@ GROUP BY 1`);
           }
           filterable
           filtered={datasourcesFilter}
-          onFilteredChange={(filtered, column) => {
+          onFilteredChange={filtered => {
             this.setState({ datasourcesFilter: filtered });
           }}
           columns={[
@@ -670,6 +685,17 @@ GROUP BY 1`);
               show: hiddenColumns.exists('Availability'),
             },
             {
+              Header: 'Segment load/drop',
+              id: 'load-drop',
+              accessor: 'num_segments_to_load',
+              filterable: false,
+              Cell: row => {
+                const { num_segments_to_load, num_segments_to_drop } = row.original;
+                return formatLoadDrop(num_segments_to_load, num_segments_to_drop);
+              },
+              show: hiddenColumns.exists('Segment load/drop'),
+            },
+            {
               Header: 'Retention',
               id: 'retention',
               accessor: row => row.rules.length,
@@ -789,7 +815,7 @@ GROUP BY 1`);
             <Button
               icon={IconNames.APPLICATION}
               text="Go to SQL"
-              onClick={() => goToQuery(this.datasourceQueryManager.getLastQuery())}
+              onClick={() => goToQuery(DatasourcesView.DATASOURCE_SQL)}
             />
           )}
           <Switch
diff --git a/web-console/src/views/home-view/home-view.tsx b/web-console/src/views/home-view/home-view.tsx
index 44daa88..1babeb3 100644
--- a/web-console/src/views/home-view/home-view.tsx
+++ b/web-console/src/views/home-view/home-view.tsx
@@ -19,10 +19,12 @@
 import { Card, H5, Icon } from '@blueprintjs/core';
 import { IconName, IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
+import { sum } from 'd3-array';
 import React from 'react';
 
 import { UrlBaser } from '../../singletons/url-baser';
-import { getHeadProp, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../utils';
+import { lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../utils';
+import { deepGet } from '../../utils/object-change';
 
 import './home-view.scss';
 
@@ -35,7 +37,7 @@ export interface CardOptions {
   error?: string | null;
 }
 
-export interface HomeViewProps extends React.Props<any> {
+export interface HomeViewProps {
   noSqlMode: boolean;
 }
 
@@ -50,6 +52,7 @@ export interface HomeViewState {
 
   segmentCountLoading: boolean;
   segmentCount: number;
+  unavailableSegmentCount: number;
   segmentCountError: string | null;
 
   supervisorCountLoading: boolean;
@@ -77,12 +80,12 @@ export interface HomeViewState {
 }
 
 export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState> {
-  private statusQueryManager: QueryManager<string, any>;
-  private datasourceQueryManager: QueryManager<string, any>;
-  private segmentQueryManager: QueryManager<string, any>;
-  private supervisorQueryManager: QueryManager<string, any>;
-  private taskQueryManager: QueryManager<string, any>;
-  private serverQueryManager: QueryManager<string, any>;
+  private statusQueryManager: QueryManager<null, any>;
+  private datasourceQueryManager: QueryManager<boolean, any>;
+  private segmentQueryManager: QueryManager<boolean, any>;
+  private supervisorQueryManager: QueryManager<null, any>;
+  private taskQueryManager: QueryManager<boolean, any>;
+  private serverQueryManager: QueryManager<boolean, any>;
 
   constructor(props: HomeViewProps, context: any) {
     super(props, context);
@@ -97,6 +100,7 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
 
       segmentCountLoading: false,
       segmentCount: 0,
+      unavailableSegmentCount: 0,
       segmentCountError: null,
 
       supervisorCountLoading: false,
@@ -122,13 +126,9 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
       peonCount: 0,
       serverCountError: null,
     };
-  }
-
-  componentDidMount(): void {
-    const { noSqlMode } = this.props;
 
     this.statusQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async () => {
         const statusResp = await axios.get('/status');
         return statusResp.data;
       },
@@ -141,15 +141,13 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
       },
     });
 
-    this.statusQueryManager.runQuery(`dummy`);
-
-    // -------------------------
-
     this.datasourceQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async noSqlMode => {
         let datasources: string[];
         if (!noSqlMode) {
-          datasources = await queryDruidSql({ query });
+          datasources = await queryDruidSql({
+            query: `SELECT datasource FROM sys.segments GROUP BY 1`,
+          });
         } else {
           const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
           datasources = datasourcesResp.data;
@@ -165,45 +163,45 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
       },
     });
 
-    this.datasourceQueryManager.runQuery(`SELECT datasource FROM sys.segments GROUP BY 1`);
-
-    // -------------------------
-
     this.segmentQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async noSqlMode => {
         if (noSqlMode) {
           const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
           const loadstatus = loadstatusResp.data;
-          const unavailableSegmentNum = Object.keys(loadstatus).reduce((sum, key) => {
-            return sum + loadstatus[key];
-          }, 0);
+          const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
 
           const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple');
           const datasourcesMeta = datasourcesMetaResp.data;
-          const availableSegmentNum = datasourcesMeta.reduce((sum: number, curr: any) => {
-            return sum + curr.properties.segments.count;
-          }, 0);
+          const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
+            deepGet(curr, 'properties.segments.count'),
+          );
 
-          return availableSegmentNum + unavailableSegmentNum;
+          return {
+            count: availableSegmentNum + unavailableSegmentNum,
+            unavailable: unavailableSegmentNum,
+          };
         } else {
-          const segments = await queryDruidSql({ query });
-          return getHeadProp(segments, 'count') || 0;
+          const segments = await queryDruidSql({
+            query: `SELECT
+  COUNT(*) as "count",
+  COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
+FROM sys.segments`,
+          });
+          return segments.length === 1 ? segments[0] : null;
         }
       },
       onStateChange: ({ result, loading, error }) => {
         this.setState({
           segmentCountLoading: loading,
-          segmentCount: result,
+          segmentCount: result ? result.count : 0,
+          unavailableSegmentCount: result ? result.unavailable : 0,
           segmentCountError: error,
         });
       },
     });
 
-    this.segmentQueryManager.runQuery(`SELECT COUNT(*) as "count" FROM sys.segments`);
-
-    // -------------------------
     this.supervisorQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async () => {
         const resp = await axios.get('/druid/indexer/v1/supervisor?full');
         const data = resp.data;
         const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
@@ -223,12 +221,8 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
       },
     });
 
-    this.supervisorQueryManager.runQuery('dummy');
-
-    // -------------------------
-
     this.taskQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async noSqlMode => {
         if (noSqlMode) {
           const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
           const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
@@ -243,7 +237,11 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
           };
         } else {
           const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({
-            query,
+            query: `SELECT
+  CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
+  COUNT (*) AS "count"
+FROM sys.tasks
+GROUP BY 1`,
           });
           return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
         }
@@ -261,16 +259,8 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
       },
     });
 
-    this.taskQueryManager.runQuery(`SELECT
-  CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
-  COUNT (*) AS "count"
-FROM sys.tasks
-GROUP BY 1`);
-
-    // -------------------------
-
     this.serverQueryManager = new QueryManager({
-      processQuery: async query => {
+      processQuery: async noSqlMode => {
         if (noSqlMode) {
           const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
           const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
@@ -283,7 +273,9 @@ GROUP BY 1`);
           const serverCountsFromQuery: {
             server_type: string;
             count: number;
-          }[] = await queryDruidSql({ query });
+          }[] = await queryDruidSql({
+            query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
+          });
           return lookupBy(serverCountsFromQuery, x => x.server_type, x => x.count);
         }
       },
@@ -301,10 +293,17 @@ GROUP BY 1`);
         });
       },
     });
+  }
 
-    this.serverQueryManager.runQuery(
-      `SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
-    );
+  componentDidMount(): void {
+    const { noSqlMode } = this.props;
+
+    this.statusQueryManager.runQuery(null);
+    this.datasourceQueryManager.runQuery(noSqlMode);
+    this.segmentQueryManager.runQuery(noSqlMode);
+    this.supervisorQueryManager.runQuery(null);
+    this.taskQueryManager.runQuery(noSqlMode);
+    this.serverQueryManager.runQuery(noSqlMode);
   }
 
   componentWillUnmount(): void {
@@ -362,7 +361,14 @@ GROUP BY 1`);
           icon: IconNames.STACKED_CHART,
           title: 'Segments',
           loading: state.segmentCountLoading,
-          content: pluralIfNeeded(state.segmentCount, 'segment'),
+          content: (
+            <>
+              <p>{pluralIfNeeded(state.segmentCount, 'segment')}</p>
+              {Boolean(state.unavailableSegmentCount) && (
+                <p>{pluralIfNeeded(state.unavailableSegmentCount, 'unavailable segment')}</p>
+              )}
+            </>
+          ),
           error: state.datasourceCountError,
         })}
         {this.renderCard({
diff --git a/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap b/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
index 9718f80..c7e8108 100644
--- a/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
+++ b/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
@@ -153,7 +153,7 @@ exports[`load data view matches snapshot 1`] = `
     </div>
   </div>
   <div
-    className="main"
+    className="main bp3-input"
   />
   <div
     className="control"
diff --git a/web-console/src/views/load-data-view/filter-table/filter-table.tsx b/web-console/src/views/load-data-view/filter-table/filter-table.tsx
index eacd9b5..25756f7 100644
--- a/web-console/src/views/load-data-view/filter-table/filter-table.tsx
+++ b/web-console/src/views/load-data-view/filter-table/filter-table.tsx
@@ -22,12 +22,12 @@ import ReactTable from 'react-table';
 
 import { TableCell } from '../../../components';
 import { caseInsensitiveContains, filterMap } from '../../../utils';
-import { DruidFilter, Transform } from '../../../utils/ingestion-spec';
-import { HeaderAndRows } from '../../../utils/sampler';
+import { DruidFilter } from '../../../utils/ingestion-spec';
+import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
 
 import './filter-table.scss';
 
-export interface FilterTableProps extends React.Props<any> {
+export interface FilterTableProps {
   sampleData: HeaderAndRows;
   columnFilter: string;
   dimensionFilters: DruidFilter[];
@@ -82,7 +82,7 @@ export class FilterTable extends React.PureComponent<FilterTableProps> {
             headerClassName: columnClassName,
             className: columnClassName,
             id: String(i),
-            accessor: row => (row.parsed ? row.parsed[columnName] : null),
+            accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
             Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
           };
         })}
diff --git a/web-console/src/views/load-data-view/load-data-view.scss b/web-console/src/views/load-data-view/load-data-view.scss
index 847bea4..b94e892 100644
--- a/web-console/src/views/load-data-view/load-data-view.scss
+++ b/web-console/src/views/load-data-view/load-data-view.scss
@@ -19,7 +19,7 @@
 .load-data-view {
   height: 100%;
   display: grid;
-  grid-gap: 10px 5px;
+  grid-gap: 15px 5px;
   grid-template-columns: 1fr 280px;
   grid-template-rows: 55px 1fr 28px;
   grid-template-areas:
@@ -29,7 +29,8 @@
 
   &.welcome {
     .main {
-      margin-left: -10px;
+      height: 100%;
+      padding: 0;
 
       .bp3-card {
         position: relative;
@@ -37,8 +38,8 @@
         vertical-align: top;
         width: 250px;
         height: 140px;
-        margin-left: 15px;
-        margin-bottom: 15px;
+        margin-top: 10px;
+        margin-left: 10px;
         font-size: 16px;
         text-align: center;
 
diff --git a/web-console/src/views/load-data-view/load-data-view.spec.tsx b/web-console/src/views/load-data-view/load-data-view.spec.tsx
index 399a7a4..7ff02da 100644
--- a/web-console/src/views/load-data-view/load-data-view.spec.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.spec.tsx
@@ -23,7 +23,7 @@ import { LoadDataView } from './load-data-view';
 
 describe('load data view', () => {
   it('matches snapshot', () => {
-    const loadDataView = shallow(<LoadDataView goToTask={(taskId: string | null) => {}} />);
+    const loadDataView = shallow(<LoadDataView goToTask={() => {}} />);
     expect(loadDataView).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx b/web-console/src/views/load-data-view/load-data-view.tsx
index 38f24e6..85200b7 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -25,8 +25,6 @@ import {
   Card,
   Classes,
   Code,
-  Dialog,
-  Elevation,
   FormGroup,
   H5,
   HTMLSelect,
@@ -221,10 +219,10 @@ const VIEW_TITLE: Record<Step, string> = {
   loading: 'Loading',
 };
 
-export interface LoadDataViewProps extends React.Props<any> {
+export interface LoadDataViewProps {
   initSupervisorId?: string | null;
   initTaskId?: string | null;
-  goToTask: (taskId: string | null, supervisor?: string) => void;
+  goToTask: (taskId: string | undefined, supervisor?: string) => void;
 }
 
 export interface LoadDataViewState {
@@ -524,7 +522,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
 
     return (
       <>
-        <div className="main">
+        <div className="main bp3-input">
           {this.renderIngestionCard('kafka')}
           {this.renderIngestionCard('kinesis')}
           {this.renderIngestionCard('index:static-s3')}
@@ -679,7 +677,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
             <Button
               text="Submit task"
               rightIcon={IconNames.ARROW_RIGHT}
-              onClick={() => goToTask(null, 'task')}
+              onClick={() => goToTask(undefined, 'task')}
               intent={Intent.PRIMARY}
             />
           </FormGroup>
@@ -695,7 +693,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
               <Button
                 text="Submit supervisor"
                 rightIcon={IconNames.ARROW_RIGHT}
-                onClick={() => goToTask(null, 'supervisor')}
+                onClick={() => goToTask(undefined, 'supervisor')}
                 intent={Intent.PRIMARY}
               />
             </FormGroup>
@@ -703,7 +701,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
               <Button
                 text="Submit task"
                 rightIcon={IconNames.ARROW_RIGHT}
-                onClick={() => goToTask(null, 'task')}
+                onClick={() => goToTask(undefined, 'task')}
                 intent={Intent.PRIMARY}
               />
             </FormGroup>
@@ -1602,15 +1600,44 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
       return;
     }
 
+    if (sampleResponse.data.length) {
+      this.setState({
+        cacheKey: sampleResponse.cacheKey,
+        filterQueryState: new QueryState({
+          data: headerAndRowsFromSampleResponse(
+            sampleResponse,
+            undefined,
+            ['__time'].concat(parserColumns),
+            true,
+          ),
+        }),
+      });
+      return;
+    }
+
+    // The filters matched no data
+    let sampleResponseNoFilter: SampleResponse;
+    try {
+      const specNoFilter = deepSet(spec, 'dataSchema.transformSpec.filter', null);
+      sampleResponseNoFilter = await sampleForFilter(specNoFilter, sampleStrategy, cacheKey);
+    } catch (e) {
+      this.setState({
+        filterQueryState: new QueryState({ error: e.message }),
+      });
+      return;
+    }
+
+    const headerAndRowsNoFilter = headerAndRowsFromSampleResponse(
+      sampleResponseNoFilter,
+      undefined,
+      ['__time'].concat(parserColumns),
+      true,
+    );
+
     this.setState({
-      cacheKey: sampleResponse.cacheKey,
+      cacheKey: sampleResponseNoFilter.cacheKey,
       filterQueryState: new QueryState({
-        data: headerAndRowsFromSampleResponse(
-          sampleResponse,
-          undefined,
-          ['__time'].concat(parserColumns),
-          true,
-        ),
+        data: deepSet(headerAndRowsNoFilter, 'rows', []),
       }),
     });
   }
@@ -2684,7 +2711,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
                 });
 
                 setTimeout(() => {
-                  goToTask(null);
+                  goToTask(undefined); // Can we get the supervisor ID here?
                 }, 1000);
               }
             }}
diff --git a/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx b/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx
index 0cee551..b30d08b 100644
--- a/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx
+++ b/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx
@@ -27,7 +27,7 @@ import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
 
 import './parse-data-table.scss';
 
-export interface ParseDataTableProps extends React.Props<any> {
+export interface ParseDataTableProps {
   sampleData: HeaderAndRows;
   columnFilter: string;
   canFlatten: boolean;
diff --git a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.scss b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.scss
index f9910e1..3122c37 100644
--- a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.scss
+++ b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.scss
@@ -17,4 +17,5 @@
  */
 
 .parse-time-table {
+  position: relative;
 }
diff --git a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx
index 7bdb79e..4a8c3e1 100644
--- a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx
+++ b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx
@@ -32,7 +32,7 @@ import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
 
 import './parse-time-table.scss';
 
-export interface ParseTimeTableProps extends React.Props<any> {
+export interface ParseTimeTableProps {
   sampleBundle: {
     headerAndRows: HeaderAndRows;
     timestampSpec: TimestampSpec;
diff --git a/web-console/src/views/load-data-view/schema-table/schema-table.tsx b/web-console/src/views/load-data-view/schema-table/schema-table.tsx
index 554fff7..aac4e56 100644
--- a/web-console/src/views/load-data-view/schema-table/schema-table.tsx
+++ b/web-console/src/views/load-data-view/schema-table/schema-table.tsx
@@ -21,12 +21,7 @@ import React from 'react';
 import ReactTable from 'react-table';
 
 import { TableCell } from '../../../components';
-import {
-  alphanumericCompare,
-  caseInsensitiveContains,
-  filterMap,
-  sortWithPrefixSuffix,
-} from '../../../utils';
+import { caseInsensitiveContains, filterMap, sortWithPrefixSuffix } from '../../../utils';
 import {
   DimensionSpec,
   DimensionsSpec,
@@ -35,13 +30,12 @@ import {
   getMetricSpecName,
   inflateDimensionSpec,
   MetricSpec,
-  TimestampSpec,
 } from '../../../utils/ingestion-spec';
 import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
 
 import './schema-table.scss';
 
-export interface SchemaTableProps extends React.Props<any> {
+export interface SchemaTableProps {
   sampleBundle: {
     headerAndRows: HeaderAndRows;
     dimensionsSpec: DimensionsSpec;
@@ -103,7 +97,7 @@ export class SchemaTable extends React.PureComponent<SchemaTableProps> {
               headerClassName: columnClassName,
               className: columnClassName,
               id: String(i),
-              accessor: row => (row.parsed ? row.parsed[columnName] : null),
+              accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
               Cell: row => <TableCell value={row.value} />,
             };
           } else {
diff --git a/web-console/src/views/load-data-view/transform-table/transform-table.tsx b/web-console/src/views/load-data-view/transform-table/transform-table.tsx
index 83b7ed5..36e721e 100644
--- a/web-console/src/views/load-data-view/transform-table/transform-table.tsx
+++ b/web-console/src/views/load-data-view/transform-table/transform-table.tsx
@@ -24,11 +24,11 @@ import { TableCell } from '../../../components';
 import { caseInsensitiveContains, filterMap } from '../../../utils';
 import { escapeColumnName } from '../../../utils/druid-expression';
 import { Transform } from '../../../utils/ingestion-spec';
-import { HeaderAndRows } from '../../../utils/sampler';
+import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
 
 import './transform-table.scss';
 
-export interface TransformTableProps extends React.Props<any> {
+export interface TransformTableProps {
   sampleData: HeaderAndRows;
   columnFilter: string;
   transformedColumnsOnly: boolean;
@@ -91,7 +91,7 @@ export class TransformTable extends React.PureComponent<TransformTableProps> {
             headerClassName: columnClassName,
             className: columnClassName,
             id: String(i),
-            accessor: row => (row.parsed ? row.parsed[columnName] : null),
+            accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
             Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
           };
         })}
diff --git a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
index a4a93cb..228fed1 100755
--- a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
+++ b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
@@ -7,10 +7,9 @@ exports[`lookups view matches snapshot 1`] = `
   <ViewControlBar
     label="Lookups"
   >
-    <Blueprint3.Button
-      icon="refresh"
-      onClick={[Function]}
-      text="Refresh"
+    <RefreshButton
+      localStorageKey="lookups-refresh-rate"
+      onRefresh={[Function]}
     />
     <Blueprint3.Button
       icon="plus"
@@ -90,28 +89,28 @@ exports[`lookups view matches snapshot 1`] = `
       Array [
         Object {
           "Header": "Lookup name",
-          "accessor": [Function],
+          "accessor": "id",
           "filterable": true,
           "id": "lookup_name",
           "show": true,
         },
         Object {
           "Header": "Tier",
-          "accessor": [Function],
+          "accessor": "tier",
           "filterable": true,
           "id": "tier",
           "show": true,
         },
         Object {
           "Header": "Type",
-          "accessor": [Function],
+          "accessor": "spec.type",
           "filterable": true,
           "id": "type",
           "show": true,
         },
         Object {
           "Header": "Version",
-          "accessor": [Function],
+          "accessor": "version",
           "filterable": true,
           "id": "version",
           "show": true,
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx b/web-console/src/views/lookups-view/lookups-view.tsx
index 182ea93..a792cbb 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -16,18 +16,17 @@
  * limitations under the License.
  */
 
-import { Button, Icon, Intent, Popover, Position } from '@blueprintjs/core';
+import { Button, Intent } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
-import classNames from 'classnames';
 import React from 'react';
 import ReactTable from 'react-table';
 
-import { ActionCell, TableColumnSelector, ViewControlBar } from '../../components';
+import { ActionCell, RefreshButton, TableColumnSelector, ViewControlBar } from '../../components';
 import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/';
 import { AppToaster } from '../../singletons/toaster';
 import { getDruidErrorMessage, LocalStorageKeys, QueryManager } from '../../utils';
-import { BasicAction, basicActionsToMenu } from '../../utils/basic-action';
+import { BasicAction } from '../../utils/basic-action';
 import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
 
 import './lookups-view.scss';
@@ -36,7 +35,7 @@ const tableColumns: string[] = ['Lookup name', 'Tier', 'Type', 'Version', Action
 
 const DEFAULT_LOOKUP_TIER: string = '__default';
 
-export interface LookupsViewProps extends React.Props<any> {}
+export interface LookupsViewProps {}
 
 export interface LookupsViewState {
   lookups: {}[] | null;
@@ -58,7 +57,7 @@ export interface LookupsViewState {
 }
 
 export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsViewState> {
-  private lookupsGetQueryManager: QueryManager<string, { lookupEntries: any[]; tiers: string[] }>;
+  private lookupsQueryManager: QueryManager<null, { lookupEntries: any[]; tiers: string[] }>;
 
   constructor(props: LookupsViewProps, context: any) {
     super(props, context);
@@ -82,11 +81,9 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
         LocalStorageKeys.LOOKUP_TABLE_COLUMN_SELECTION,
       ),
     };
-  }
 
-  componentDidMount(): void {
-    this.lookupsGetQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+    this.lookupsQueryManager = new QueryManager({
+      processQuery: async () => {
         const tiersResp = await axios.get('/druid/coordinator/v1/lookups/config?discover=true');
         const tiers =
           tiersResp.data && tiersResp.data.length > 0 ? tiersResp.data : [DEFAULT_LOOKUP_TIER];
@@ -121,18 +118,20 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
         });
       },
     });
+  }
 
-    this.lookupsGetQueryManager.runQuery('dummy');
+  componentDidMount(): void {
+    this.lookupsQueryManager.runQuery(null);
   }
 
   componentWillUnmount(): void {
-    this.lookupsGetQueryManager.terminate();
+    this.lookupsQueryManager.terminate();
   }
 
   private async initializeLookup() {
     try {
       await axios.post(`/druid/coordinator/v1/lookups/config`, {});
-      this.lookupsGetQueryManager.rerunLastQuery();
+      this.lookupsQueryManager.rerunLastQuery();
     } catch (e) {
       AppToaster.show({
         icon: IconNames.ERROR,
@@ -208,7 +207,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
       this.setState({
         lookupEditDialogOpen: false,
       });
-      this.lookupsGetQueryManager.rerunLastQuery();
+      this.lookupsQueryManager.rerunLastQuery();
     } catch (e) {
       AppToaster.show({
         icon: IconNames.ERROR,
@@ -254,7 +253,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
         intent={Intent.DANGER}
         onClose={success => {
           this.setState({ deleteLookupTier: null, deleteLookupName: null });
-          if (success) this.lookupsGetQueryManager.rerunLastQuery();
+          if (success) this.lookupsQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to delete the lookup '${deleteLookupName}'?`}</p>
@@ -296,28 +295,28 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
             {
               Header: 'Lookup name',
               id: 'lookup_name',
-              accessor: (row: any) => row.id,
+              accessor: 'id',
               filterable: true,
               show: hiddenColumns.exists('Lookup name'),
             },
             {
               Header: 'Tier',
               id: 'tier',
-              accessor: (row: any) => row.tier,
+              accessor: 'tier',
               filterable: true,
               show: hiddenColumns.exists('Tier'),
             },
             {
               Header: 'Type',
               id: 'type',
-              accessor: (row: any) => row.spec.type,
+              accessor: 'spec.type',
               filterable: true,
               show: hiddenColumns.exists('Type'),
             },
             {
               Header: 'Version',
               id: 'version',
-              accessor: (row: any) => row.version,
+              accessor: 'version',
               filterable: true,
               show: hiddenColumns.exists('Version'),
             },
@@ -325,7 +324,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               Header: ActionCell.COLUMN_LABEL,
               id: ActionCell.COLUMN_ID,
               width: ActionCell.COLUMN_WIDTH,
-              accessor: row => ({ id: row.id, tier: row.tier }),
+              accessor: (row: any) => ({ id: row.id, tier: row.tier }),
               filterable: false,
               Cell: (row: any) => {
                 const lookupId = row.value.id;
@@ -375,10 +374,9 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
     return (
       <div className="lookups-view app-view">
         <ViewControlBar label="Lookups">
-          <Button
-            icon={IconNames.REFRESH}
-            text="Refresh"
-            onClick={() => this.lookupsGetQueryManager.rerunLastQuery()}
+          <RefreshButton
+            onRefresh={() => this.lookupsQueryManager.rerunLastQuery()}
+            localStorageKey={LocalStorageKeys.LOOKUPS_REFRESH_RATE}
           />
           {!lookupsError && (
             <Button
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 c540026..f95708a 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
@@ -26,7 +26,7 @@ import { ColumnMetadata } from '../../../utils/column-metadata';
 
 import './column-tree.scss';
 
-export interface ColumnTreeProps extends React.Props<any> {
+export interface ColumnTreeProps {
   columnMetadataLoading: boolean;
   columnMetadata: ColumnMetadata[] | null;
   onQueryStringChange: (queryString: string) => void;
@@ -150,11 +150,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
     );
   }
 
-  private handleNodeClick = (
-    nodeData: ITreeNode,
-    nodePath: number[],
-    e: React.MouseEvent<HTMLElement>,
-  ) => {
+  private handleNodeClick = (nodeData: ITreeNode, nodePath: number[]) => {
     const { onQueryStringChange } = this.props;
     const { columnTree, selectedTreeIndex } = this.state;
     if (!columnTree) return;
diff --git a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
index d682195..d4dbbd6 100644
--- a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
+++ b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
@@ -44,7 +44,7 @@ export interface QueryExtraInfoData {
   wrappedLimit?: number;
 }
 
-export interface QueryExtraInfoProps extends React.Props<any> {
+export interface QueryExtraInfoProps {
   queryExtraInfo: QueryExtraInfoData;
   onDownload: (filename: string, format: string) => void;
 }
diff --git a/web-console/src/views/query-view/query-input/query-input.tsx b/web-console/src/views/query-view/query-input/query-input.tsx
index 407b1c4..6df25e8 100644
--- a/web-console/src/views/query-view/query-input/query-input.tsx
+++ b/web-console/src/views/query-view/query-input/query-input.tsx
@@ -33,7 +33,7 @@ import './query-input.scss';
 
 const langTools = ace.acequire('ace/ext/language_tools');
 
-export interface QueryInputProps extends React.Props<any> {
+export interface QueryInputProps {
   queryString: string;
   onQueryStringChange: (newQueryString: string) => void;
   runeMode: boolean;
@@ -71,7 +71,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
       );
 
       langTools.addCompleter({
-        getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
+        getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
           callback(null, completions);
         },
       });
@@ -103,7 +103,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
     );
 
     const keywordCompleter = {
-      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
+      getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
         return callback(null, keywordList);
       },
     };
@@ -137,7 +137,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
     });
 
     langTools.addCompleter({
-      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
+      getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
         callback(null, functionList);
       },
       getDocTooltip: (item: any) => {
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 ad3fdbc..e35856a 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
@@ -24,7 +24,7 @@ import { HeaderRows } from '../../../utils';
 
 import './query-output.scss';
 
-export interface QueryOutputProps extends React.Props<any> {
+export interface QueryOutputProps {
   loading: boolean;
   result: HeaderRows | null;
   error: string | null;
diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx
index 3e16b84..6395409 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -56,8 +56,8 @@ interface QueryWithContext {
   wrapQuery?: boolean;
 }
 
-export interface QueryViewProps extends React.Props<any> {
-  initQuery: string | null;
+export interface QueryViewProps {
+  initQuery: string | undefined;
 }
 
 export interface QueryViewState {
@@ -119,7 +119,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     }
   }
 
-  private metadataQueryManager: QueryManager<string, ColumnMetadata[]>;
+  private metadataQueryManager: QueryManager<null, ColumnMetadata[]>;
   private sqlQueryManager: QueryManager<QueryWithContext, QueryResult>;
   private explainQueryManager: QueryManager<
     QueryWithContext,
@@ -146,11 +146,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
       explainResult: null,
       explainError: null,
     };
-  }
 
-  componentDidMount(): void {
     this.metadataQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async () => {
         return await queryDruidSql<ColumnMetadata>({
           query: `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS`,
         });
@@ -170,10 +168,8 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
       },
     });
 
-    this.metadataQueryManager.runQuery('dummy');
-
     this.sqlQueryManager = new QueryManager({
-      processQuery: async (queryWithContext: QueryWithContext) => {
+      processQuery: async (queryWithContext: QueryWithContext): Promise<QueryResult> => {
         const { queryString, queryContext, wrapQuery } = queryWithContext;
         let queryId: string | null = null;
         let sqlQueryId: string | null = null;
@@ -274,6 +270,10 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     });
   }
 
+  componentDidMount(): void {
+    this.metadataQueryManager.runQuery(null);
+  }
+
   componentWillUnmount(): void {
     this.metadataQueryManager.terminate();
     this.sqlQueryManager.terminate();
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 d750614..f987c13 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
@@ -43,7 +43,7 @@ import {
 } from '../../../utils/query-context';
 import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
 
-export interface RunButtonProps extends React.Props<any> {
+export interface RunButtonProps {
   runeMode: boolean;
   queryContext: QueryContext;
   onQueryContextChange: (newQueryContext: QueryContext) => void;
diff --git a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index 9be7408..efd5139 100755
--- a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -13,7 +13,7 @@ exports[`segments-view matches snapshot 1`] = `
         onRefresh={[Function]}
       />
       <Blueprint3.Button
-        hidden={false}
+        disabled={true}
         icon="application"
         onClick={[Function]}
         text="Go to SQL"
diff --git a/web-console/src/views/segments-view/segments-view.spec.tsx b/web-console/src/views/segments-view/segments-view.spec.tsx
index 464ce2e..fc8b4d8 100644
--- a/web-console/src/views/segments-view/segments-view.spec.tsx
+++ b/web-console/src/views/segments-view/segments-view.spec.tsx
@@ -27,7 +27,7 @@ describe('segments-view', () => {
       <SegmentsView
         datasource={'test'}
         onlyUnavailable={false}
-        goToQuery={(initSql: string) => {}}
+        goToQuery={() => {}}
         noSqlMode={false}
       />,
     );
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 5c0b397..8be5c05 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -32,7 +32,6 @@ import {
   formatNumber,
   LocalStorageKeys,
   makeBooleanFilter,
-  parseList,
   queryDruidSql,
   QueryManager,
   sqlQueryCustomTableFilter,
@@ -68,10 +67,10 @@ const tableColumnsNoSql: string[] = [
   'Size',
 ];
 
-export interface SegmentsViewProps extends React.Props<any> {
+export interface SegmentsViewProps {
   goToQuery: (initSql: string) => void;
-  datasource: string | null;
-  onlyUnavailable: boolean | null;
+  datasource: string | undefined;
+  onlyUnavailable: boolean | undefined;
   noSqlMode: boolean;
 }
 
@@ -113,7 +112,7 @@ interface SegmentQueryResultRow {
 
 export class SegmentsView extends React.PureComponent<SegmentsViewProps, SegmentsViewState> {
   private segmentsSqlQueryManager: QueryManager<QueryAndSkip, SegmentQueryResultRow[]>;
-  private segmentsJsonQueryManager: QueryManager<any, SegmentQueryResultRow[]>;
+  private segmentsNoSqlQueryManager: QueryManager<null, SegmentQueryResultRow[]>;
 
   constructor(props: SegmentsViewProps, context: any) {
     super(props, context);
@@ -158,8 +157,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
       },
     });
 
-    this.segmentsJsonQueryManager = new QueryManager({
-      processQuery: async (query: any) => {
+    this.segmentsNoSqlQueryManager = new QueryManager({
+      processQuery: async () => {
         const datasourceList = (await axios.get('/druid/coordinator/v1/metadata/datasources')).data;
         const nestedResults: SegmentQueryResultRow[][] = await Promise.all(
           datasourceList.map(async (d: string) => {
@@ -185,7 +184,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             });
           }),
         );
-        const results: SegmentQueryResultRow[] = [].concat
+        const results: SegmentQueryResultRow[] = ([] as SegmentQueryResultRow[]).concat
           .apply([], nestedResults)
           .sort((d1: any, d2: any) => {
             return d2.start.localeCompare(d1.start);
@@ -205,16 +204,16 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
 
   componentDidMount(): void {
     if (this.props.noSqlMode) {
-      this.segmentsJsonQueryManager.runQuery('init');
+      this.segmentsNoSqlQueryManager.runQuery(null);
     }
   }
 
   componentWillUnmount(): void {
     this.segmentsSqlQueryManager.terminate();
-    this.segmentsJsonQueryManager.terminate();
+    this.segmentsNoSqlQueryManager.terminate();
   }
 
-  private fetchData = (state: any, instance: any) => {
+  private fetchData = (state: any) => {
     const { page, pageSize, filtered, sorted } = state;
     const totalQuerySize = (page + 1) * pageSize;
 
@@ -256,7 +255,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
     });
   };
 
-  private fecthClientSideData = (state: any, instance: any) => {
+  private fetchClientSideData = (state: any) => {
     const { page, pageSize, filtered, sorted } = state;
     const { allSegments } = this.state;
     if (allSegments == null) return;
@@ -312,10 +311,10 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         filterable
         filtered={segmentFilter}
         defaultSorted={[{ id: 'start', desc: true }]}
-        onFilteredChange={(filtered, column) => {
+        onFilteredChange={filtered => {
           this.setState({ segmentFilter: filtered });
         }}
-        onFetchData={noSqlMode ? this.fecthClientSideData : this.fetchData}
+        onFetchData={noSqlMode ? this.fetchClientSideData : this.fetchData}
         showPageJump={false}
         ofText=""
         columns={[
@@ -407,7 +406,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             accessor: 'num_rows',
             filterable: false,
             defaultSortDesc: true,
-            Cell: row => formatNumber(row.value),
+            Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>),
             show: !noSqlMode && hiddenColumns.exists('Num rows'),
           },
           {
@@ -456,8 +455,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
               if (row.aggregated) return '';
               const id = row.value;
               const datasource = row.row.datasource;
-              const dimensions = parseList(row.original.payload.dimensions);
-              const metrics = parseList(row.original.payload.metrics);
               return (
                 <ActionCell
                   onDetail={() => {
@@ -471,7 +468,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                 />
               );
             },
-            Aggregated: row => '',
+            Aggregated: () => '',
             show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
           },
         ]}
@@ -503,7 +500,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         onClose={success => {
           this.setState({ terminateSegmentId: null });
           if (success) {
-            this.segmentsJsonQueryManager.rerunLastQuery();
+            this.segmentsNoSqlQueryManager.rerunLastQuery();
             this.segmentsSqlQueryManager.rerunLastQuery();
           }
         }}
@@ -522,6 +519,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
       hiddenColumns,
     } = this.state;
     const { goToQuery, noSqlMode } = this.props;
+    const lastSegmentsQuery = this.segmentsSqlQueryManager.getLastQuery();
 
     return (
       <>
@@ -530,7 +528,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             <RefreshButton
               onRefresh={auto =>
                 noSqlMode
-                  ? this.segmentsJsonQueryManager.rerunLastQueryInBackground(auto)
+                  ? this.segmentsNoSqlQueryManager.rerunLastQueryInBackground(auto)
                   : this.segmentsSqlQueryManager.rerunLastQueryInBackground(auto)
               }
               localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
@@ -539,8 +537,11 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
               <Button
                 icon={IconNames.APPLICATION}
                 text="Go to SQL"
-                hidden={noSqlMode}
-                onClick={() => goToQuery(this.segmentsSqlQueryManager.getLastQuery().query)}
+                disabled={!lastSegmentsQuery}
+                onClick={() => {
+                  if (!lastSegmentsQuery) return;
+                  goToQuery(lastSegmentsQuery.query);
+                }}
               />
             )}
             <TableColumnSelector
diff --git a/web-console/src/views/servers-view/servers-view.spec.tsx b/web-console/src/views/servers-view/servers-view.spec.tsx
index 0e3ae2e..f96987c 100644
--- a/web-console/src/views/servers-view/servers-view.spec.tsx
+++ b/web-console/src/views/servers-view/servers-view.spec.tsx
@@ -26,8 +26,8 @@ describe('servers view', () => {
     const serversView = shallow(
       <ServersView
         middleManager={'test'}
-        goToQuery={(initSql: string) => {}}
-        goToTask={(taskId: string) => {}}
+        goToQuery={() => {}}
+        goToTask={() => {}}
         noSqlMode={false}
       />,
     );
diff --git a/web-console/src/views/servers-view/servers-view.tsx b/web-console/src/views/servers-view/servers-view.tsx
index e8a0433..01c8319 100644
--- a/web-console/src/views/servers-view/servers-view.tsx
+++ b/web-console/src/views/servers-view/servers-view.tsx
@@ -74,8 +74,8 @@ function formatQueues(
   return queueParts.join(', ') || 'Empty load/drop queues';
 }
 
-export interface ServersViewProps extends React.Props<any> {
-  middleManager: string | null;
+export interface ServersViewProps {
+  middleManager: string | undefined;
   goToQuery: (initSql: string) => void;
   goToTask: (taskId: string) => void;
   noSqlMode: boolean;
@@ -133,25 +133,33 @@ interface ServerResultRow
     Partial<MiddleManagerQueryResultRow> {}
 
 export class ServersView extends React.PureComponent<ServersViewProps, ServersViewState> {
-  private serverQueryManager: QueryManager<string, ServerQueryResultRow[]>;
-
-  constructor(props: ServersViewProps, context: any) {
-    super(props, context);
-    this.state = {
-      serversLoading: true,
-      servers: null,
-      serversError: null,
-      serverFilter: [],
-      groupServersBy: null,
-
-      middleManagerDisableWorkerHost: null,
-      middleManagerEnableWorkerHost: null,
-
-      hiddenColumns: new LocalStorageBackedArray<string>(
-        LocalStorageKeys.SERVER_TABLE_COLUMN_SELECTION,
-      ),
-    };
-  }
+  private serverQueryManager: QueryManager<boolean, ServerResultRow[]>;
+
+  // Ranking
+  //   coordinator => 7
+  //   overlord => 6
+  //   router => 5
+  //   broker => 4
+  //   historical => 3
+  //   middle_manager => 2
+  //   peon => 1
+
+  static SERVER_SQL = `SELECT
+  "server", "server_type", "tier", "host", "plaintext_port", "tls_port", "curr_size", "max_size",
+  (
+    CASE "server_type"
+    WHEN 'coordinator' THEN 7
+    WHEN 'overlord' THEN 6
+    WHEN 'router' THEN 5
+    WHEN 'broker' THEN 4
+    WHEN 'historical' THEN 3
+    WHEN 'middle_manager' THEN 2
+    WHEN 'peon' THEN 1
+    ELSE 0
+    END
+  ) AS "rank"
+FROM sys.servers
+ORDER BY "rank" DESC, "server" DESC`;
 
   static async getServers(): Promise<ServerQueryResultRow[]> {
     const allServerResp = await axios.get('/druid/coordinator/v1/servers?simple');
@@ -170,15 +178,28 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
     });
   }
 
-  componentDidMount(): void {
-    const { noSqlMode } = this.props;
-    const { hiddenColumns } = this.state;
+  constructor(props: ServersViewProps, context: any) {
+    super(props, context);
+    this.state = {
+      serversLoading: true,
+      servers: null,
+      serversError: null,
+      serverFilter: [],
+      groupServersBy: null,
+
+      middleManagerDisableWorkerHost: null,
+      middleManagerEnableWorkerHost: null,
+
+      hiddenColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.SERVER_TABLE_COLUMN_SELECTION,
+      ),
+    };
 
     this.serverQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async noSqlMode => {
         let servers: ServerQueryResultRow[];
         if (!noSqlMode) {
-          servers = await queryDruidSql({ query });
+          servers = await queryDruidSql({ query: ServersView.SERVER_SQL });
         } else {
           servers = await ServersView.getServers();
         }
@@ -229,32 +250,11 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
         });
       },
     });
+  }
 
-    // Ranking
-    //   coordinator => 7
-    //   overlord => 6
-    //   router => 5
-    //   broker => 4
-    //   historical => 3
-    //   middle_manager => 2
-    //   peon => 1
-
-    this.serverQueryManager.runQuery(`SELECT
-  "server", "server_type", "tier", "host", "plaintext_port", "tls_port", "curr_size", "max_size",
-  (
-    CASE "server_type"
-    WHEN 'coordinator' THEN 7
-    WHEN 'overlord' THEN 6
-    WHEN 'router' THEN 5
-    WHEN 'broker' THEN 4
-    WHEN 'historical' THEN 3
-    WHEN 'middle_manager' THEN 2
-    WHEN 'peon' THEN 1
-    ELSE 0
-    END
-  ) AS "rank"
-FROM sys.servers
-ORDER BY "rank" DESC, "server" DESC`);
+  componentDidMount(): void {
+    const { noSqlMode } = this.props;
+    this.serverQueryManager.runQuery(noSqlMode);
   }
 
   componentWillUnmount(): void {
@@ -291,7 +291,7 @@ ORDER BY "rank" DESC, "server" DESC`);
         }
         filterable
         filtered={serverFilter}
-        onFilteredChange={(filtered, column) => {
+        onFilteredChange={filtered => {
           this.setState({ serverFilter: filtered });
         }}
         pivotBy={groupServersBy ? [groupServersBy] : []}
@@ -301,7 +301,7 @@ ORDER BY "rank" DESC, "server" DESC`);
             Header: 'Server',
             accessor: 'server',
             width: 300,
-            Aggregated: row => '',
+            Aggregated: () => '',
             show: hiddenColumns.exists('Server'),
           },
           {
@@ -654,7 +654,7 @@ ORDER BY "rank" DESC, "server" DESC`);
             <Button
               icon={IconNames.APPLICATION}
               text="Go to SQL"
-              onClick={() => goToQuery(this.serverQueryManager.getLastQuery())}
+              onClick={() => goToQuery(ServersView.SERVER_SQL)}
             />
           )}
           <TableColumnSelector
diff --git a/web-console/src/views/task-view/tasks-view.spec.tsx b/web-console/src/views/task-view/tasks-view.spec.tsx
index 137dfe9..254f2ce 100644
--- a/web-console/src/views/task-view/tasks-view.spec.tsx
+++ b/web-console/src/views/task-view/tasks-view.spec.tsx
@@ -27,8 +27,8 @@ describe('tasks view', () => {
       <TasksView
         openDialog={'test'}
         taskId={'test'}
-        goToQuery={(initSql: string) => null}
-        goToMiddleManager={(middleManager: string) => null}
+        goToQuery={() => null}
+        goToMiddleManager={() => null}
         goToLoadDataView={() => null}
         noSqlMode={false}
       />,
diff --git a/web-console/src/views/task-view/tasks-view.tsx b/web-console/src/views/task-view/tasks-view.tsx
index 3ab7293..39bb7f9 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -45,7 +45,6 @@ import { AppToaster } from '../../singletons/toaster';
 import {
   addFilter,
   booleanCustomTableFilter,
-  countBy,
   formatDuration,
   getDruidErrorMessage,
   localStorageGet,
@@ -77,9 +76,9 @@ const taskTableColumns: string[] = [
   ActionCell.COLUMN_LABEL,
 ];
 
-export interface TasksViewProps extends React.Props<any> {
-  taskId: string | null;
-  openDialog: string | null;
+export interface TasksViewProps {
+  taskId: string | undefined;
+  openDialog: string | undefined;
   goToQuery: (initSql: string) => void;
   goToMiddleManager: (middleManager: string) => void;
   goToLoadDataView: (supervisorId?: string, taskId?: string) => void;
@@ -172,8 +171,8 @@ function stateToColor(status: string): string {
 }
 
 export class TasksView extends React.PureComponent<TasksViewProps, TasksViewState> {
-  private supervisorQueryManager: QueryManager<string, SupervisorQueryResultRow[]>;
-  private taskQueryManager: QueryManager<string, TaskQueryResultRow[]>;
+  private supervisorQueryManager: QueryManager<null, SupervisorQueryResultRow[]>;
+  private taskQueryManager: QueryManager<boolean, TaskQueryResultRow[]>;
   static statusRanking: Record<string, number> = {
     RUNNING: 4,
     PENDING: 3,
@@ -182,6 +181,18 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
     FAILED: 1,
   };
 
+  static TASK_SQL = `SELECT
+  "task_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
+  CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
+  (
+    CASE WHEN "status" = 'RUNNING' THEN
+     (CASE "runner_status" WHEN 'RUNNING' THEN 4 WHEN 'PENDING' THEN 3 ELSE 2 END)
+    ELSE 1
+    END
+  ) AS "rank"
+FROM sys.tasks
+ORDER BY "rank" DESC, "created_time" DESC`;
+
   constructor(props: TasksViewProps, context: any) {
     super(props, context);
     this.state = {
@@ -220,34 +231,9 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
         LocalStorageKeys.SUPERVISOR_TABLE_COLUMN_SELECTION,
       ),
     };
-  }
-
-  static parseTasks = (data: any[]): TaskQueryResultRow[] => {
-    return data.map((d: any) => {
-      return {
-        created_time: d.createdTime,
-        datasource: d.dataSource,
-        duration: d.duration ? d.duration : 0,
-        error_msg: d.errorMsg,
-        location: d.location.host ? `${d.location.host}:${d.location.port}` : null,
-        status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
-        task_id: d.id,
-        type: d.typTasksView,
-        rank:
-          TasksView.statusRanking[d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode],
-      };
-    });
-  };
-
-  private onSecondaryPaneSizeChange(secondaryPaneSize: number) {
-    localStorageSet(LocalStorageKeys.TASKS_VIEW_PANE_SIZE, String(secondaryPaneSize));
-  }
-
-  componentDidMount(): void {
-    const { noSqlMode } = this.props;
 
     this.supervisorQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async () => {
         const resp = await axios.get('/druid/indexer/v1/supervisor?full');
         return resp.data;
       },
@@ -260,12 +246,12 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
       },
     });
 
-    this.supervisorQueryManager.runQuery('dummy');
-
     this.taskQueryManager = new QueryManager({
-      processQuery: async (query: string) => {
+      processQuery: async noSqlMode => {
         if (!noSqlMode) {
-          return await queryDruidSql({ query });
+          return await queryDruidSql({
+            query: TasksView.TASK_SQL,
+          });
         } else {
           const taskEndpoints: string[] = [
             'completeTasks',
@@ -279,7 +265,7 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
               return TasksView.parseTasks(resp.data);
             }),
           );
-          return [].concat.apply([], result);
+          return ([] as TaskQueryResultRow[]).concat.apply([], result);
         }
       },
       onStateChange: ({ result, loading, error }) => {
@@ -290,25 +276,34 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
         });
       },
     });
+  }
 
-    // Ranking
-    //   RUNNING => 4
-    //   PENDING => 3
-    //   WAITING => 2
-    //   SUCCESS => 1
-    //   FAILED => 1
+  static parseTasks = (data: any[]): TaskQueryResultRow[] => {
+    return data.map((d: any) => {
+      return {
+        created_time: d.createdTime,
+        datasource: d.dataSource,
+        duration: d.duration ? d.duration : 0,
+        error_msg: d.errorMsg,
+        location: d.location.host ? `${d.location.host}:${d.location.port}` : null,
+        status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
+        task_id: d.id,
+        type: d.typTasksView,
+        rank:
+          TasksView.statusRanking[d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode],
+      };
+    });
+  };
 
-    this.taskQueryManager.runQuery(`SELECT
-  "task_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
-  CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
-  (
-    CASE WHEN "status" = 'RUNNING' THEN
-     (CASE "runner_status" WHEN 'RUNNING' THEN 4 WHEN 'PENDING' THEN 3 ELSE 2 END)
-    ELSE 1
-    END
-  ) AS "rank"
-FROM sys.tasks
-ORDER BY "rank" DESC, "created_time" DESC`);
+  private onSecondaryPaneSizeChange(secondaryPaneSize: number) {
+    localStorageSet(LocalStorageKeys.TASKS_VIEW_PANE_SIZE, String(secondaryPaneSize));
+  }
+
+  componentDidMount(): void {
+    const { noSqlMode } = this.props;
+
+    this.supervisorQueryManager.runQuery(null);
+    this.taskQueryManager.runQuery(noSqlMode);
   }
 
   componentWillUnmount(): void {
@@ -687,7 +682,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
           noDataText={!tasksLoading && tasks && !tasks.length ? 'No tasks' : tasksError || ''}
           filterable
           filtered={taskFilter}
-          onFilteredChange={(filtered, column) => {
+          onFilteredChange={filtered => {
             this.setState({ taskFilter: filtered });
           }}
           defaultSorted={[{ id: 'status', desc: true }]}
@@ -697,7 +692,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               Header: 'Task ID',
               accessor: 'task_id',
               width: 300,
-              Aggregated: row => '',
+              Aggregated: () => '',
               show: hiddenTaskColumns.exists('Task ID'),
             },
             {
@@ -738,7 +733,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
             {
               Header: 'Location',
               accessor: 'location',
-              Aggregated: row => '',
+              Aggregated: () => '',
               filterMethod: (filter: Filter, row: any) => {
                 return booleanCustomTableFilter(filter, row.location);
               },
@@ -748,7 +743,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               Header: 'Created time',
               accessor: 'created_time',
               width: 120,
-              Aggregated: row => '',
+              Aggregated: () => '',
               show: hiddenTaskColumns.exists('Created time'),
             },
             {
@@ -844,7 +839,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                   />
                 );
               },
-              Aggregated: row => '',
+              Aggregated: () => '',
               show: hiddenTaskColumns.exists(ActionCell.COLUMN_LABEL),
             },
           ]}
@@ -969,7 +964,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                 <Button
                   icon={IconNames.APPLICATION}
                   text="Go to SQL"
-                  onClick={() => goToQuery(this.taskQueryManager.getLastQuery())}
+                  onClick={() => goToQuery(TasksView.TASK_SQL)}
                 />
               )}
               <Popover content={submitTaskMenu} position={Position.BOTTOM_LEFT}>
diff --git a/web-console/tsconfig.json b/web-console/tsconfig.json
index 510a2ea..255116f 100644
--- a/web-console/tsconfig.json
+++ b/web-console/tsconfig.json
@@ -1,17 +1,18 @@
 {
   "compilerOptions": {
+    "forceConsistentCasingInFileNames": true,
     "experimentalDecorators": true,
-    "noImplicitAny": true,
     "noEmitOnError": true,
     "declaration": false,
     "removeComments": true,
     "noFallthroughCasesInSwitch": true,
     "noImplicitReturns": true,
-    "strictNullChecks": true,
-    "strictFunctionTypes": false,
+    "strict": true,
     "skipLibCheck": true,
     "importHelpers": true,
     "esModuleInterop": true,
+    "noUnusedParameters": true,
+    "noUnusedLocals": true,
     "target": "es5",
     "module": "commonjs",
     "moduleResolution": "node",
diff --git a/web-console/tslint.json b/web-console/tslint.json
index eb60339..4b3e15e 100644
--- a/web-console/tslint.json
+++ b/web-console/tslint.json
@@ -8,7 +8,6 @@
     "no-string-literal": false,
     "object-literal-shorthand": false,
     "arrow-return-shorthand": false,
-    "no-var-requires": false,
     "no-unused-variable": false
   },
   "linterOptions": {
diff --git a/web-console/unified-console.html b/web-console/unified-console.html
index 2567875..b1c8fd4 100644
--- a/web-console/unified-console.html
+++ b/web-console/unified-console.html
@@ -27,6 +27,6 @@
 <body class="bp3-dark mouse-mode">
   <div class="app-container"></div>
   <script src="console-config.js"></script>
-  <script src="public/web-console-0.15.0.js"></script>
+  <script src="public/web-console-0.16.0.js"></script>
 </body>
 </html>
diff --git a/web-console/webpack.config.js b/web-console/webpack.config.js
index 6c67603..22456f0 100644
--- a/web-console/webpack.config.js
+++ b/web-console/webpack.config.js
@@ -23,6 +23,15 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
 
 const { version } = require('./package.json');
 
+function friendlyErrorFormatter(e, colors) {
+  //const messageColor = error.severity === "warning" ? colors.bold.yellow : colors.bold.red;
+  // return (
+  //   "Does not compute.... " +
+  //   messageColor(Object.keys(error).map(key => `${key}: ${error[key]}`))
+  // );
+  return `${e.severity}: ${e.content} [TS${e.code}]\n    at (${e.file}:${e.line}:${e.character})`;
+}
+
 module.exports = (env) => {
   let druidUrl = ((env || {}).druid_host || process.env.druid_host || 'localhost');
   if (!druidUrl.startsWith('http')) druidUrl = 'http://' + druidUrl;
@@ -77,8 +86,15 @@ module.exports = (env) => {
         },
         {
           test: /\.tsx?$/,
-          use: 'ts-loader',
-          exclude: /node_modules/
+          exclude: /node_modules/,
+          use: [
+            {
+              loader: 'ts-loader',
+              options: {
+                errorFormatter: friendlyErrorFormatter
+              }
+            }
+          ]
         },
         {
           test: /\.s?css$/,


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