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

[incubator-druid] branch master updated: Data loader (GUI component) (#7572)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new baf54f3  Data loader (GUI component) (#7572)
baf54f3 is described below

commit baf54f373cec583d5f93c4f7d6cf6c25869eefd7
Author: Vadim Ogievetsky <va...@gmail.com>
AuthorDate: Fri May 3 17:14:57 2019 -0700

    Data loader (GUI component) (#7572)
    
    * data loader init
    
    * fix timecolumn text
    
    * feedback changes
    
    * fixing typos and improving error reporting
    
    * added local firehose warning
    
    * update warning copy
    
    * refine copy
    
    * better copy
    
    * fix tests
    
    * remove console log
    
    * copy change
    
    * add banner message
---
 web-console/package-lock.json                      | 1536 ++++++-------
 web-console/package.json                           |   15 +-
 web-console/script/mkcomp                          |  135 ++
 web-console/src/components/array-input.tsx         |   58 +
 web-console/src/components/auto-form.scss          |   18 +
 web-console/src/components/auto-form.tsx           |  216 +-
 .../{auto-form.scss => center-message.scss}        |   15 +-
 .../center-message.tsx}                            |   33 +-
 .../{entry.scss => components/clearable-input.tsx} |   47 +-
 .../sql-view.scss => components/external-link.tsx} |   35 +-
 web-console/src/components/header-bar.scss         |    9 +-
 web-console/src/components/header-bar.tsx          |   56 +-
 .../{auto-form.scss => null-table-cell.scss}       |   14 +-
 .../{entry.scss => components/null-table-cell.tsx} |   48 +-
 web-console/src/components/sql-control.tsx         |   17 +-
 web-console/src/console-application.tsx            |  140 +-
 web-console/src/dialogs/about-dialog.tsx           |   11 +-
 web-console/src/dialogs/async-action-dialog.tsx    |    4 +-
 .../src/dialogs/coordinator-dynamic-config.tsx     |    3 +-
 .../src/dialogs/overlord-dynamic-config.tsx        |    3 +-
 ...ion-dialog.test.ts => retention-dialog.spec.ts} |    1 -
 web-console/src/entry.scss                         |   21 +
 .../auto-form.scss => utils/druid-expression.ts}   |   15 +-
 .../src/utils/{druid-query.tsx => druid-query.ts}  |   33 +-
 web-console/src/utils/druid-time.ts                |   72 +
 web-console/src/utils/druid-type.ts                |   98 +
 web-console/src/utils/example-ingestion-spec.ts    |   97 +
 web-console/src/utils/general.tsx                  |   19 +
 web-console/src/utils/index.tsx                    |    1 +
 web-console/src/utils/ingestion-spec.tsx           | 1143 ++++++++++
 web-console/src/utils/local-storage-keys.tsx       |    1 +
 web-console/src/utils/object-change.spec.ts        |  167 ++
 web-console/src/utils/object-change.ts             |  119 +
 web-console/src/utils/query-manager.tsx            |   12 +-
 .../header-bar.scss => utils/query-state.ts}       |   43 +-
 web-console/src/utils/sampler.ts                   |  342 +++
 web-console/src/utils/spec-utils.spec.ts           |   76 +
 web-console/src/utils/spec-utils.ts                |   70 +
 web-console/src/variables.ts                       |    1 +
 web-console/src/views/datasource-view.tsx          |    2 +-
 web-console/src/views/load-data-view.scss          |  257 +++
 web-console/src/views/load-data-view.tsx           | 2353 ++++++++++++++++++++
 web-console/src/views/sql-view.scss                |    4 -
 web-console/src/views/sql-view.tsx                 |    7 +-
 web-console/src/views/tasks-view.tsx               |   71 +-
 web-console/tsconfig.json                          |    3 +-
 web-console/webpack.config.js                      |   21 +-
 47 files changed, 6325 insertions(+), 1137 deletions(-)

diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index d3abb42..8a6638c 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -14,18 +14,18 @@
       }
     },
     "@babel/core": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.3.4.tgz",
-      "integrity": "sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==",
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.3.tgz",
+      "integrity": "sha512-oDpASqKFlbspQfzAE7yaeTmdljSH2ADIvBlb0RwbStltTuWa0+7CCI1fYVINNv9saHPa1W7oaKeuNuKj+RQCvA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.3.4",
-        "@babel/helpers": "^7.2.0",
-        "@babel/parser": "^7.3.4",
-        "@babel/template": "^7.2.2",
-        "@babel/traverse": "^7.3.4",
-        "@babel/types": "^7.3.4",
+        "@babel/generator": "^7.4.0",
+        "@babel/helpers": "^7.4.3",
+        "@babel/parser": "^7.4.3",
+        "@babel/template": "^7.4.0",
+        "@babel/traverse": "^7.4.3",
+        "@babel/types": "^7.4.0",
         "convert-source-map": "^1.1.0",
         "debug": "^4.1.0",
         "json5": "^2.1.0",
@@ -62,24 +62,18 @@
       }
     },
     "@babel/generator": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz",
-      "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.0.tgz",
+      "integrity": "sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.3.4",
+        "@babel/types": "^7.4.0",
         "jsesc": "^2.5.1",
         "lodash": "^4.17.11",
         "source-map": "^0.5.0",
         "trim-right": "^1.0.1"
       },
       "dependencies": {
-        "jsesc": {
-          "version": "2.5.2",
-          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-          "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-          "dev": true
-        },
         "source-map": {
           "version": "0.5.7",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -115,23 +109,23 @@
       "dev": true
     },
     "@babel/helper-split-export-declaration": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz",
-      "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz",
+      "integrity": "sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.0.0"
+        "@babel/types": "^7.4.0"
       }
     },
     "@babel/helpers": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz",
-      "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==",
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.3.tgz",
+      "integrity": "sha512-BMh7X0oZqb36CfyhvtbSmcWc3GXocfxv3yNsAEuM0l+fAqSO22rQrUpijr3oE/10jCTrB6/0b9kzmG4VetCj8Q==",
       "dev": true,
       "requires": {
-        "@babel/template": "^7.1.2",
-        "@babel/traverse": "^7.1.5",
-        "@babel/types": "^7.3.0"
+        "@babel/template": "^7.4.0",
+        "@babel/traverse": "^7.4.3",
+        "@babel/types": "^7.4.0"
       }
     },
     "@babel/highlight": {
@@ -146,9 +140,9 @@
       }
     },
     "@babel/parser": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz",
-      "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==",
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.3.tgz",
+      "integrity": "sha512-gxpEUhTS1sGA63EGQGuA+WESPR/6tz6ng7tSHFCmaTJK/cGK8y37cBTspX+U2xCAue2IQVvF6Z0oigmjwD8YGQ==",
       "dev": true
     },
     "@babel/plugin-syntax-object-rest-spread": {
@@ -161,36 +155,36 @@
       }
     },
     "@babel/runtime": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
-      "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz",
+      "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==",
       "requires": {
-        "regenerator-runtime": "^0.12.0"
+        "regenerator-runtime": "^0.13.2"
       }
     },
     "@babel/template": {
-      "version": "7.2.2",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
-      "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.0.tgz",
+      "integrity": "sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@babel/parser": "^7.2.2",
-        "@babel/types": "^7.2.2"
+        "@babel/parser": "^7.4.0",
+        "@babel/types": "^7.4.0"
       }
     },
     "@babel/traverse": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.3.4.tgz",
-      "integrity": "sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==",
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.3.tgz",
+      "integrity": "sha512-HmA01qrtaCwwJWpSKpA948cBvU5BrmviAief/b3AVw936DtcdsTexlbyzNuDnthwhOQ37xshn7hvQaEQk7ISYQ==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.3.4",
+        "@babel/generator": "^7.4.0",
         "@babel/helper-function-name": "^7.1.0",
-        "@babel/helper-split-export-declaration": "^7.0.0",
-        "@babel/parser": "^7.3.4",
-        "@babel/types": "^7.3.4",
+        "@babel/helper-split-export-declaration": "^7.4.0",
+        "@babel/parser": "^7.4.3",
+        "@babel/types": "^7.4.0",
         "debug": "^4.1.0",
         "globals": "^11.1.0",
         "lodash": "^4.17.11"
@@ -208,9 +202,9 @@
       }
     },
     "@babel/types": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-      "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.0.tgz",
+      "integrity": "sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA==",
       "dev": true,
       "requires": {
         "esutils": "^2.0.2",
@@ -441,9 +435,9 @@
       "dev": true
     },
     "@types/babel__core": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.0.tgz",
-      "integrity": "sha512-wJTeJRt7BToFx3USrCDs2BhEi4ijBInTQjOIukj6a/5tEkwpFMVZ+1ppgmE+Q/FQyc5P/VWUbx7I9NELrKruHA==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.1.tgz",
+      "integrity": "sha512-+hjBtgcFPYyCTo0A15+nxrCVJL7aC6Acg87TXd5OW3QhHswdrOLoles+ldL2Uk8q++7yIfl4tURtztccdeeyOw==",
       "dev": true,
       "requires": {
         "@babel/parser": "^7.1.0",
@@ -498,6 +492,23 @@
       "resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz",
       "integrity": "sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA=="
     },
+    "@types/events": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
+      "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
+      "dev": true
+    },
+    "@types/glob": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
+      "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
+      "dev": true,
+      "requires": {
+        "@types/events": "*",
+        "@types/minimatch": "*",
+        "@types/node": "*"
+      }
+    },
     "@types/history": {
       "version": "4.7.2",
       "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz",
@@ -546,16 +557,16 @@
         "@types/lodash": "*"
       }
     },
-    "@types/mocha": {
-      "version": "5.2.6",
-      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz",
-      "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==",
+    "@types/minimatch": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+      "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
       "dev": true
     },
     "@types/node": {
-      "version": "11.13.4",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.4.tgz",
-      "integrity": "sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==",
+      "version": "11.13.7",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.7.tgz",
+      "integrity": "sha512-suFHr6hcA9mp8vFrZTgrmqW2ZU3mbWsryQtQlY/QvwTISCw7nw/j+bCQPPohqmskhmqa5wLNuMHTTsc+xf1MQg==",
       "dev": true
     },
     "@types/numeral": {
@@ -565,15 +576,15 @@
       "dev": true
     },
     "@types/prop-types": {
-      "version": "15.7.0",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.0.tgz",
-      "integrity": "sha512-eItQyV43bj4rR3JPV0Skpl1SncRCdziTEK9/v8VwXmV6d/qOUO8/EuWeHBbCZcsfSHfzI5UyMJLCSXtxxznyZg==",
+      "version": "15.7.1",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
+      "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==",
       "dev": true
     },
     "@types/react": {
-      "version": "16.8.13",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.13.tgz",
-      "integrity": "sha512-otJ4ntMuHGrvm67CdDJMAls4WqotmAmW0g3HmWi9LCjSWXrxoXY/nHXrtmMfvPEEmGFNm6NdgMsJmnfH820Qaw==",
+      "version": "16.8.14",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.14.tgz",
+      "integrity": "sha512-26tFVJ1omGmzIdFTFmnC5zhz1GTaqCjxgUxV4KzWvsybF42P7/j4RBn6UeO3KbHPXqKWZszMXMoI65xIWm954A==",
       "dev": true,
       "requires": {
         "@types/prop-types": "*",
@@ -653,9 +664,9 @@
       }
     },
     "@types/yargs": {
-      "version": "12.0.11",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.11.tgz",
-      "integrity": "sha512-IsU1TD+8cQCyG76ZqxP0cVFnofvfzT8p/wO8ENT4jbN/KKN3grsHFgHNl/U+08s33ayX4LwI85cEhYXCOlOkMw==",
+      "version": "12.0.12",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz",
+      "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==",
       "dev": true
     },
     "@webassemblyjs/ast": {
@@ -881,9 +892,9 @@
       "dev": true
     },
     "acorn-globals": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz",
-      "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==",
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz",
+      "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==",
       "dev": true,
       "requires": {
         "acorn": "^6.0.1",
@@ -905,9 +916,9 @@
       "dev": true
     },
     "ajv": {
-      "version": "6.9.2",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.2.tgz",
-      "integrity": "sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg==",
+      "version": "6.10.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+      "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
       "dev": true,
       "requires": {
         "fast-deep-equal": "^2.0.1",
@@ -935,9 +946,9 @@
       "dev": true
     },
     "ansi-colors": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
-      "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
+      "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
       "dev": true
     },
     "ansi-escapes": {
@@ -975,6 +986,17 @@
       "requires": {
         "micromatch": "^3.1.4",
         "normalize-path": "^2.1.1"
+      },
+      "dependencies": {
+        "normalize-path": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+          "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+          "dev": true,
+          "requires": {
+            "remove-trailing-separator": "^1.0.1"
+          }
+        }
       }
     },
     "append-transform": {
@@ -1048,9 +1070,9 @@
       "dev": true
     },
     "array-flatten": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
-      "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
       "dev": true
     },
     "array-includes": {
@@ -1168,9 +1190,9 @@
       }
     },
     "async-each": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
-      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+      "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
       "dev": true
     },
     "async-foreach": {
@@ -1243,65 +1265,6 @@
         "is-buffer": "^1.1.5"
       }
     },
-    "babel-code-frame": {
-      "version": "6.26.0",
-      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
-      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
-      "dev": true,
-      "requires": {
-        "chalk": "^1.1.3",
-        "esutils": "^2.0.2",
-        "js-tokens": "^3.0.2"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
-          "dev": true
-        },
-        "ansi-styles": {
-          "version": "2.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
-          "dev": true
-        },
-        "chalk": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^2.2.1",
-            "escape-string-regexp": "^1.0.2",
-            "has-ansi": "^2.0.0",
-            "strip-ansi": "^3.0.0",
-            "supports-color": "^2.0.0"
-          }
-        },
-        "js-tokens": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
-          "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
-          "dev": true
-        },
-        "strip-ansi": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^2.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
-          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
-          "dev": true
-        }
-      }
-    },
     "babel-jest": {
       "version": "24.7.1",
       "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.7.1.tgz",
@@ -1318,14 +1281,14 @@
       }
     },
     "babel-plugin-istanbul": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.1.tgz",
-      "integrity": "sha512-RNNVv2lsHAXJQsEJ5jonQwrJVWK8AcZpG1oxhnjCUaAjL7xahYLANhPUZbzEQHjKy1NMYUwn+0NPKQc8iSY4xQ==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.2.tgz",
+      "integrity": "sha512-U3ZVajC+Z69Gim7ZzmD4Wcsq76i/1hqDamBfowc1tWzWjybRy70iWfngP2ME+1CrgcgZ/+muIbPY/Yi0dxdIkQ==",
       "dev": true,
       "requires": {
         "find-up": "^3.0.0",
-        "istanbul-lib-instrument": "^3.0.0",
-        "test-exclude": "^5.0.0"
+        "istanbul-lib-instrument": "^3.2.0",
+        "test-exclude": "^5.2.2"
       }
     },
     "babel-plugin-jest-hoist": {
@@ -1435,6 +1398,18 @@
         "tweetnacl": "^0.14.3"
       }
     },
+    "bfj": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz",
+      "integrity": "sha512-+GUNvzHR4nRyGybQc2WpNJL4MJazMuvf92ueIyA0bIkPRwhhQu3IfZQ2PSoVPpCBJfmoSdOxu5rnotfFLlvYRQ==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.5.1",
+        "check-types": "^7.3.0",
+        "hoopy": "^0.1.2",
+        "tryer": "^1.0.0"
+      }
+    },
     "big.js": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -1442,9 +1417,9 @@
       "dev": true
     },
     "binary-extensions": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz",
-      "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==",
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+      "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
       "dev": true
     },
     "block-stream": {
@@ -1524,6 +1499,14 @@
         "dns-txt": "^2.0.2",
         "multicast-dns": "^6.0.1",
         "multicast-dns-service-types": "^1.1.0"
+      },
+      "dependencies": {
+        "array-flatten": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
+          "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
+          "dev": true
+        }
       }
     },
     "brace": {
@@ -1599,12 +1582,6 @@
         }
       }
     },
-    "browser-stdout": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
-      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
-      "dev": true
-    },
     "browserify-aes": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -1677,14 +1654,14 @@
       }
     },
     "browserslist": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.5.4.tgz",
-      "integrity": "sha512-rAjx494LMjqKnMPhFkuLmLp8JWEX0o8ADTGeAbOqaF+XCvYLreZrG5uVjnPBlAQ8REZK4pzXGvp0bWgrFtKaag==",
+      "version": "4.5.5",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.5.5.tgz",
+      "integrity": "sha512-0QFO1r/2c792Ohkit5XI8Cm8pDtZxgNl2H6HU4mHrpYz7314pEYcsAVVatM0l/YmxPnEzh9VygXouj4gkFUTKA==",
       "dev": true,
       "requires": {
-        "caniuse-lite": "^1.0.30000955",
-        "electron-to-chromium": "^1.3.122",
-        "node-releases": "^1.1.13"
+        "caniuse-lite": "^1.0.30000960",
+        "electron-to-chromium": "^1.3.124",
+        "node-releases": "^1.1.14"
       }
     },
     "bs-logger": {
@@ -1849,15 +1826,15 @@
       }
     },
     "callsites": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz",
-      "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
       "dev": true
     },
     "camelcase": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
-      "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
       "dev": true
     },
     "camelcase-keys": {
@@ -1879,9 +1856,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30000957",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000957.tgz",
-      "integrity": "sha512-8wxNrjAzyiHcLXN/iunskqQnJquQQ6VX8JHfW5kLgAPRSiSuKZiNfmIkP5j7jgyXqAQBSoXyJxfnbCFS0ThSiQ==",
+      "version": "1.0.30000962",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000962.tgz",
+      "integrity": "sha512-WXYsW38HK+6eaj5IZR16Rn91TGhU3OhbwjKZvJ4HN/XBIABLKfbij9Mnd3pM0VEwZSlltWjoWg3I8FQ0DGgNOA==",
       "dev": true
     },
     "capture-exit": {
@@ -1951,10 +1928,16 @@
       "integrity": "sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ==",
       "dev": true
     },
+    "check-types": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.4.0.tgz",
+      "integrity": "sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg==",
+      "dev": true
+    },
     "chokidar": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
-      "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
+      "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
       "dev": true,
       "requires": {
         "anymatch": "^2.0.0",
@@ -1968,15 +1951,7 @@
         "normalize-path": "^3.0.0",
         "path-is-absolute": "^1.0.0",
         "readdirp": "^2.2.1",
-        "upath": "^1.1.0"
-      },
-      "dependencies": {
-        "normalize-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-          "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-          "dev": true
-        }
+        "upath": "^1.1.1"
       }
     },
     "chownr": {
@@ -2141,9 +2116,9 @@
       }
     },
     "commander": {
-      "version": "2.19.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
-      "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
+      "version": "2.20.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+      "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
       "dev": true
     },
     "commondir": {
@@ -2159,9 +2134,9 @@
       "dev": true
     },
     "component-emitter": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
-      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
       "dev": true
     },
     "compressible": {
@@ -2421,6 +2396,25 @@
       "requires": {
         "postcss": "^7.0.6",
         "postcss-selector-parser": "^5.0.0-rc.4"
+      },
+      "dependencies": {
+        "cssesc": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
+          "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
+          "dev": true
+        },
+        "postcss-selector-parser": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
+          "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+          "dev": true,
+          "requires": {
+            "cssesc": "^2.0.0",
+            "indexes-of": "^1.0.1",
+            "uniq": "^1.0.1"
+          }
+        }
       }
     },
     "css-loader": {
@@ -2440,20 +2434,6 @@
         "postcss-modules-values": "^2.0.0",
         "postcss-value-parser": "^3.3.0",
         "schema-utils": "^1.0.0"
-      },
-      "dependencies": {
-        "camelcase": {
-          "version": "5.3.1",
-          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-          "dev": true
-        },
-        "normalize-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-          "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-          "dev": true
-        }
       }
     },
     "css-parse": {
@@ -2499,9 +2479,9 @@
       }
     },
     "csstype": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.3.tgz",
-      "integrity": "sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg==",
+      "version": "2.6.4",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz",
+      "integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==",
       "dev": true
     },
     "currently-unhandled": {
@@ -2929,6 +2909,12 @@
       "resolved": "https://registry.npmjs.org/druid-console/-/druid-console-0.0.2.tgz",
       "integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
     },
+    "duplexer": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+      "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
+      "dev": true
+    },
     "duplexify": {
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@@ -2957,10 +2943,16 @@
       "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
       "dev": true
     },
+    "ejs": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
+      "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==",
+      "dev": true
+    },
     "electron-to-chromium": {
-      "version": "1.3.124",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.124.tgz",
-      "integrity": "sha512-glecGr/kFdfeXUHOHAWvGcXrxNU+1wSO/t5B23tT1dtlvYB26GY8aHzZSWD7HqhqC800Lr+w/hQul6C5AF542w==",
+      "version": "1.3.125",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.125.tgz",
+      "integrity": "sha512-XxowpqQxJ4nDwUXHtVtmEhRqBpm2OnjBomZmZtHD0d2Eo0244+Ojezhk3sD/MBSSe2nxCdGQFRXHIsf/LUTL9A==",
       "dev": true
     },
     "elliptic": {
@@ -3346,12 +3338,6 @@
         "vary": "~1.1.2"
       },
       "dependencies": {
-        "array-flatten": {
-          "version": "1.1.1",
-          "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-          "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
-          "dev": true
-        },
         "debug": {
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -3562,6 +3548,12 @@
         "minimatch": "^3.0.3"
       }
     },
+    "filesize": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
+      "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
+      "dev": true
+    },
     "fill-range": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -3626,24 +3618,6 @@
         "commondir": "^1.0.1",
         "make-dir": "^2.0.0",
         "pkg-dir": "^3.0.0"
-      },
-      "dependencies": {
-        "make-dir": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
-          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
-          "dev": true,
-          "requires": {
-            "pify": "^4.0.1",
-            "semver": "^5.6.0"
-          }
-        },
-        "pify": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
-          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
-          "dev": true
-        }
       }
     },
     "find-up": {
@@ -3678,23 +3652,6 @@
         }
       }
     },
-    "flat": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
-      "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
-      "dev": true,
-      "requires": {
-        "is-buffer": "~2.0.3"
-      },
-      "dependencies": {
-        "is-buffer": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
-          "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==",
-          "dev": true
-        }
-      }
-    },
     "flat-cache": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
@@ -3829,14 +3786,14 @@
       "dev": true
     },
     "fsevents": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz",
-      "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==",
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz",
+      "integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==",
       "dev": true,
       "optional": true,
       "requires": {
-        "nan": "^2.9.2",
-        "node-pre-gyp": "^0.10.0"
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.12.0"
       },
       "dependencies": {
         "abbrev": {
@@ -3848,7 +3805,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -3869,12 +3827,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -3889,17 +3849,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -3908,12 +3871,12 @@
           "optional": true
         },
         "debug": {
-          "version": "2.6.9",
+          "version": "4.1.1",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "ms": "2.0.0"
+            "ms": "^2.1.1"
           }
         },
         "deep-extend": {
@@ -4016,7 +3979,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4028,6 +3992,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4042,6 +4007,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -4049,12 +4015,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -4073,29 +4041,30 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
         },
         "ms": {
-          "version": "2.0.0",
+          "version": "2.1.1",
           "bundled": true,
           "dev": true,
           "optional": true
         },
         "needle": {
-          "version": "2.2.4",
+          "version": "2.3.0",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "debug": "^2.1.2",
+            "debug": "^4.1.0",
             "iconv-lite": "^0.4.4",
             "sax": "^1.2.4"
           }
         },
         "node-pre-gyp": {
-          "version": "0.10.3",
+          "version": "0.12.0",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4123,13 +4092,13 @@
           }
         },
         "npm-bundled": {
-          "version": "1.0.5",
+          "version": "1.0.6",
           "bundled": true,
           "dev": true,
           "optional": true
         },
         "npm-packlist": {
-          "version": "1.2.0",
+          "version": "1.4.1",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4153,7 +4122,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -4165,6 +4135,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -4250,7 +4221,8 @@
         "safe-buffer": {
           "version": "5.1.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -4265,7 +4237,7 @@
           "optional": true
         },
         "semver": {
-          "version": "5.6.0",
+          "version": "5.7.0",
           "bundled": true,
           "dev": true,
           "optional": true
@@ -4286,6 +4258,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -4305,6 +4278,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -4348,12 +4322,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
@@ -4514,27 +4490,23 @@
       "dev": true
     },
     "global-modules": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
-      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+      "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
       "dev": true,
       "requires": {
-        "global-prefix": "^1.0.1",
-        "is-windows": "^1.0.1",
-        "resolve-dir": "^1.0.0"
+        "global-prefix": "^3.0.0"
       }
     },
     "global-prefix": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
-      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+      "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
       "dev": true,
       "requires": {
-        "expand-tilde": "^2.0.2",
-        "homedir-polyfill": "^1.0.1",
-        "ini": "^1.3.4",
-        "is-windows": "^1.0.1",
-        "which": "^1.2.14"
+        "ini": "^1.3.5",
+        "kind-of": "^6.0.2",
+        "which": "^1.3.1"
       }
     },
     "globals": {
@@ -4544,13 +4516,14 @@
       "dev": true
     },
     "globby": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-9.0.0.tgz",
-      "integrity": "sha512-q0qiO/p1w/yJ0hk8V9x1UXlgsXUxlGd0AHUOXZVXBO6aznDtpx7M8D1kBrCAItoPm+4l8r6ATXV1JpjY2SBQOw==",
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz",
+      "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==",
       "dev": true,
       "requires": {
+        "@types/glob": "^7.1.1",
         "array-union": "^1.0.2",
-        "dir-glob": "^2.2.1",
+        "dir-glob": "^2.2.2",
         "fast-glob": "^2.2.6",
         "glob": "^7.1.3",
         "ignore": "^4.0.3",
@@ -4606,12 +4579,6 @@
       "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
       "dev": true
     },
-    "growl": {
-      "version": "1.10.5",
-      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
-      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
-      "dev": true
-    },
     "growly": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
@@ -4623,6 +4590,24 @@
       "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
       "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
     },
+    "gzip-size": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.0.tgz",
+      "integrity": "sha512-wfSnvypBDRW94v5W3ckvvz/zFUNdJ81VgOP6tE4bPpRUcc0wGqU+y0eZjJEvKxwubJFix6P84sE8M51YWLT7rQ==",
+      "dev": true,
+      "requires": {
+        "duplexer": "^0.1.1",
+        "pify": "^4.0.1"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+          "dev": true
+        }
+      }
+    },
     "handle-thing": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
@@ -4630,9 +4615,9 @@
       "dev": true
     },
     "handlebars": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz",
-      "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
+      "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
       "dev": true,
       "requires": {
         "neo-async": "^2.6.0",
@@ -4757,12 +4742,6 @@
         "minimalistic-assert": "^1.0.1"
       }
     },
-    "he": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
-      "dev": true
-    },
     "history": {
       "version": "4.9.0",
       "resolved": "https://registry.npmjs.org/history/-/history-4.9.0.tgz",
@@ -4809,6 +4788,12 @@
         "parse-passwd": "^1.0.0"
       }
     },
+    "hoopy": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
+      "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==",
+      "dev": true
+    },
     "hosted-git-info": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
@@ -5102,14 +5087,6 @@
       "requires": {
         "default-gateway": "^4.2.0",
         "ipaddr.js": "^1.9.0"
-      },
-      "dependencies": {
-        "ipaddr.js": {
-          "version": "1.9.0",
-          "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
-          "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==",
-          "dev": true
-        }
       }
     },
     "interpret": {
@@ -5146,9 +5123,9 @@
       "dev": true
     },
     "ipaddr.js": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
-      "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=",
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
+      "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==",
       "dev": true
     },
     "is-accessor-descriptor": {
@@ -5311,15 +5288,15 @@
       "dev": true
     },
     "is-generator-fn": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.0.0.tgz",
-      "integrity": "sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+      "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
       "dev": true
     },
     "is-glob": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
-      "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
       "dev": true,
       "requires": {
         "is-extglob": "^2.1.1"
@@ -5358,27 +5335,27 @@
       "dev": true
     },
     "is-path-cwd": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.0.0.tgz",
-      "integrity": "sha512-m5dHHzpOXEiv18JEORttBO64UgTEypx99vCxQLjbBvGhOJxnTNglYoFXxwo6AbsQb79sqqycQEHv2hWkHZAijA==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz",
+      "integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==",
       "dev": true
     },
     "is-path-in-cwd": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.0.0.tgz",
-      "integrity": "sha512-6Vz5Gc9s/sDA3JBVu0FzWufm8xaBsqy1zn8Q6gmvGP6nSDMw78aS4poBNeatWjaRpTpxxLn1WOndAiOlk+qY8A==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+      "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
       "dev": true,
       "requires": {
-        "is-path-inside": "^1.0.0"
+        "is-path-inside": "^2.1.0"
       }
     },
     "is-path-inside": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
-      "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+      "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
       "dev": true,
       "requires": {
-        "path-is-inside": "^1.0.1"
+        "path-is-inside": "^1.0.2"
       }
     },
     "is-plain-obj": {
@@ -5498,45 +5475,45 @@
       "dev": true
     },
     "istanbul-api": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.1.tgz",
-      "integrity": "sha512-kVmYrehiwyeBAk/wE71tW6emzLiHGjYIiDrc8sfyty4F8M02/lrgXSm+R1kXysmF20zArvmZXjlE/mg24TVPJw==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.5.tgz",
+      "integrity": "sha512-meYk1BwDp59Pfse1TvPrkKYgVqAufbdBLEVoqvu/hLLKSaQ054ZTksbNepyc223tMnWdm6AdK2URIJJRqdP87g==",
       "dev": true,
       "requires": {
         "async": "^2.6.1",
         "compare-versions": "^3.2.1",
         "fileset": "^2.0.3",
-        "istanbul-lib-coverage": "^2.0.3",
-        "istanbul-lib-hook": "^2.0.3",
-        "istanbul-lib-instrument": "^3.1.0",
-        "istanbul-lib-report": "^2.0.4",
-        "istanbul-lib-source-maps": "^3.0.2",
-        "istanbul-reports": "^2.1.1",
-        "js-yaml": "^3.12.0",
-        "make-dir": "^1.3.0",
+        "istanbul-lib-coverage": "^2.0.4",
+        "istanbul-lib-hook": "^2.0.6",
+        "istanbul-lib-instrument": "^3.2.0",
+        "istanbul-lib-report": "^2.0.7",
+        "istanbul-lib-source-maps": "^3.0.5",
+        "istanbul-reports": "^2.2.3",
+        "js-yaml": "^3.13.0",
+        "make-dir": "^2.1.0",
         "minimatch": "^3.0.4",
         "once": "^1.4.0"
       }
     },
     "istanbul-lib-coverage": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
-      "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+      "integrity": "sha512-LXTBICkMARVgo579kWDm8SqfB6nvSDKNqIOBEjmJRnL04JvoMHCYGWaMddQnseJYtkEuEvO/sIcOxPLk9gERug==",
       "dev": true
     },
     "istanbul-lib-hook": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.3.tgz",
-      "integrity": "sha512-CLmEqwEhuCYtGcpNVJjLV1DQyVnIqavMLFHV/DP+np/g3qvdxu3gsPqYoJMXm15sN84xOlckFB3VNvRbf5yEgA==",
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.6.tgz",
+      "integrity": "sha512-829DKONApZ7UCiPXcOYWSgkFXa4+vNYoNOt3F+4uDJLKL1OotAoVwvThoEj1i8jmOj7odbYcR3rnaHu+QroaXg==",
       "dev": true,
       "requires": {
         "append-transform": "^1.0.0"
       }
     },
     "istanbul-lib-instrument": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz",
-      "integrity": "sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.2.0.tgz",
+      "integrity": "sha512-06IM3xShbNW4NgZv5AP4QH0oHqf1/ivFo8eFys0ZjPXHGldHJQWb3riYOKXqmOqfxXBfxu4B+g/iuhOPZH0RJg==",
       "dev": true,
       "requires": {
         "@babel/generator": "^7.0.0",
@@ -5544,30 +5521,38 @@
         "@babel/template": "^7.0.0",
         "@babel/traverse": "^7.0.0",
         "@babel/types": "^7.0.0",
-        "istanbul-lib-coverage": "^2.0.3",
-        "semver": "^5.5.0"
+        "istanbul-lib-coverage": "^2.0.4",
+        "semver": "^6.0.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
+          "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==",
+          "dev": true
+        }
       }
     },
     "istanbul-lib-report": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.4.tgz",
-      "integrity": "sha512-sOiLZLAWpA0+3b5w5/dq0cjm2rrNdAfHWaGhmn7XEFW6X++IV9Ohn+pnELAl9K3rfpaeBfbmH9JU5sejacdLeA==",
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.7.tgz",
+      "integrity": "sha512-wLH6beJBFbRBLiTlMOBxmb85cnVM1Vyl36N48e4e/aTKSM3WbOx7zbVIH1SQ537fhhsPbX0/C5JB4qsmyRXXyA==",
       "dev": true,
       "requires": {
-        "istanbul-lib-coverage": "^2.0.3",
-        "make-dir": "^1.3.0",
+        "istanbul-lib-coverage": "^2.0.4",
+        "make-dir": "^2.1.0",
         "supports-color": "^6.0.0"
       }
     },
     "istanbul-lib-source-maps": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz",
-      "integrity": "sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.5.tgz",
+      "integrity": "sha512-eDhZ7r6r1d1zQPVZehLc3D0K14vRba/eBYkz3rw16DLOrrTzve9RmnkcwrrkWVgO1FL3EK5knujVe5S8QHE9xw==",
       "dev": true,
       "requires": {
         "debug": "^4.1.1",
-        "istanbul-lib-coverage": "^2.0.3",
-        "make-dir": "^1.3.0",
+        "istanbul-lib-coverage": "^2.0.4",
+        "make-dir": "^2.1.0",
         "rimraf": "^2.6.2",
         "source-map": "^0.6.1"
       },
@@ -5584,9 +5569,9 @@
       }
     },
     "istanbul-reports": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.1.1.tgz",
-      "integrity": "sha512-FzNahnidyEPBCI0HcufJoSEoKykesRlFcSzQqjH9x0+LC8tnnE/p/90PBLu8iZTxr8yYZNyTtiAujUqyN+CIxw==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.3.tgz",
+      "integrity": "sha512-T6EbPuc8Cb620LWAYyZ4D8SSn06dY9i1+IgUX2lTH8gbwflMc9Obd33zHTyNX653ybjpamAHS9toKS3E6cGhTw==",
       "dev": true,
       "requires": {
         "handlebars": "^4.1.0"
@@ -6009,9 +5994,9 @@
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
     },
     "js-yaml": {
-      "version": "3.12.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
-      "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
       "dev": true,
       "requires": {
         "argparse": "^1.0.7",
@@ -6058,6 +6043,12 @@
         "xml-name-validator": "^3.0.0"
       }
     },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "dev": true
+    },
     "json-parse-better-errors": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -6131,9 +6122,9 @@
       "dev": true
     },
     "kleur": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.2.tgz",
-      "integrity": "sha512-3h7B2WRT5LNXOtQiAaWonilegHcPSf9nLVXlSTci8lu1dZUuui61+EsPEZqSVxY7rXYmB2DVKMQILxaO5WL61Q==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+      "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
       "dev": true
     },
     "known-css-properties": {
@@ -6189,18 +6180,6 @@
         "spdx-expression-parse": "^3.0.0",
         "spdx-satisfies": "^4.0.0",
         "treeify": "^1.1.0"
-      },
-      "dependencies": {
-        "nopt": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
-          "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
-          "dev": true,
-          "requires": {
-            "abbrev": "1",
-            "osenv": "^0.1.4"
-          }
-        }
       }
     },
     "load-json-file": {
@@ -6368,12 +6347,21 @@
       }
     },
     "make-dir": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
-      "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
       "dev": true,
       "requires": {
-        "pify": "^3.0.0"
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+          "dev": true
+        }
       }
     },
     "make-error": {
@@ -6472,13 +6460,13 @@
       "dev": true
     },
     "mem": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz",
-      "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+      "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
       "dev": true,
       "requires": {
         "map-age-cleaner": "^0.1.1",
-        "mimic-fn": "^1.0.0",
+        "mimic-fn": "^2.0.0",
         "p-is-promise": "^2.0.0"
       }
     },
@@ -6665,24 +6653,24 @@
       "dev": true
     },
     "mime-db": {
-      "version": "1.38.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
-      "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+      "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
       "dev": true
     },
     "mime-types": {
-      "version": "2.1.22",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
-      "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
+      "version": "2.1.24",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+      "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
       "dev": true,
       "requires": {
-        "mime-db": "~1.38.0"
+        "mime-db": "1.40.0"
       }
     },
     "mimic-fn": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
-      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
       "dev": true
     },
     "minimalistic-assert": {
@@ -6796,110 +6784,6 @@
         }
       }
     },
-    "mocha": {
-      "version": "6.1.3",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.3.tgz",
-      "integrity": "sha512-QdE/w//EPHrqgT5PNRUjRVHy6IJAzAf1R8n2O8W8K2RZ+NbPfOD5cBDp+PGa2Gptep37C/TdBiaNwakppEzEbg==",
-      "dev": true,
-      "requires": {
-        "ansi-colors": "3.2.3",
-        "browser-stdout": "1.3.1",
-        "debug": "3.2.6",
-        "diff": "3.5.0",
-        "escape-string-regexp": "1.0.5",
-        "find-up": "3.0.0",
-        "glob": "7.1.3",
-        "growl": "1.10.5",
-        "he": "1.2.0",
-        "js-yaml": "3.13.0",
-        "log-symbols": "2.2.0",
-        "minimatch": "3.0.4",
-        "mkdirp": "0.5.1",
-        "ms": "2.1.1",
-        "node-environment-flags": "1.0.5",
-        "object.assign": "4.1.0",
-        "strip-json-comments": "2.0.1",
-        "supports-color": "6.0.0",
-        "which": "1.3.1",
-        "wide-align": "1.1.3",
-        "yargs": "13.2.2",
-        "yargs-parser": "13.0.0",
-        "yargs-unparser": "1.5.0"
-      },
-      "dependencies": {
-        "get-caller-file": {
-          "version": "2.0.5",
-          "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-          "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-          "dev": true
-        },
-        "js-yaml": {
-          "version": "3.13.0",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz",
-          "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==",
-          "dev": true,
-          "requires": {
-            "argparse": "^1.0.7",
-            "esprima": "^4.0.0"
-          }
-        },
-        "require-main-filename": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-          "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-          "dev": true
-        },
-        "string-width": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^7.0.1",
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^5.1.0"
-          }
-        },
-        "supports-color": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
-          "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        },
-        "yargs": {
-          "version": "13.2.2",
-          "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz",
-          "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==",
-          "dev": true,
-          "requires": {
-            "cliui": "^4.0.0",
-            "find-up": "^3.0.0",
-            "get-caller-file": "^2.0.1",
-            "os-locale": "^3.1.0",
-            "require-directory": "^2.1.1",
-            "require-main-filename": "^2.0.0",
-            "set-blocking": "^2.0.0",
-            "string-width": "^3.0.0",
-            "which-module": "^2.0.0",
-            "y18n": "^4.0.0",
-            "yargs-parser": "^13.0.0"
-          }
-        },
-        "yargs-parser": {
-          "version": "13.0.0",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz",
-          "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==",
-          "dev": true,
-          "requires": {
-            "camelcase": "^5.0.0",
-            "decamelize": "^1.2.0"
-          }
-        }
-      }
-    },
     "move-concurrently": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -6936,9 +6820,9 @@
       "dev": true
     },
     "nan": {
-      "version": "2.12.1",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
-      "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==",
+      "version": "2.13.2",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
+      "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
       "dev": true
     },
     "nanomatch": {
@@ -6984,24 +6868,6 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
-    "node-environment-flags": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
-      "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
-      "dev": true,
-      "requires": {
-        "object.getownpropertydescriptors": "^2.0.3",
-        "semver": "^5.7.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.7.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
-          "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
-          "dev": true
-        }
-      }
-    },
     "node-fetch": {
       "version": "1.7.3",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
@@ -7037,6 +6903,15 @@
         "which": "1"
       },
       "dependencies": {
+        "nopt": {
+          "version": "3.0.6",
+          "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+          "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+          "dev": true,
+          "requires": {
+            "abbrev": "1"
+          }
+        },
         "semver": {
           "version": "5.3.0",
           "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
@@ -7110,9 +6985,9 @@
       }
     },
     "node-releases": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.13.tgz",
-      "integrity": "sha512-fKZGviSXR6YvVPyc011NHuJDSD8gFQvLPmc2d2V3BS4gr52ycyQ1Xzs7a8B+Ax3Ni/W+5h1h4SqmzeoA8WZRmA==",
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.15.tgz",
+      "integrity": "sha512-cKV097BQaZr8LTSRUa2+oc/aX5L8UkZtPQrMSTgiJEeaW7ymTDCoRaGCoaTqk0lqnalcoSHu4wjSl0Cmj2+bMw==",
       "dev": true,
       "requires": {
         "semver": "^5.3.0"
@@ -7214,12 +7089,13 @@
       }
     },
     "nopt": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
-      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+      "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
       "dev": true,
       "requires": {
-        "abbrev": "1"
+        "abbrev": "1",
+        "osenv": "^0.1.4"
       }
     },
     "normalize-package-data": {
@@ -7235,13 +7111,10 @@
       }
     },
     "normalize-path": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
-      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
-      "dev": true,
-      "requires": {
-        "remove-trailing-separator": "^1.0.1"
-      }
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
     },
     "normalize-range": {
       "version": "0.1.2",
@@ -7347,9 +7220,9 @@
       }
     },
     "object-keys": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz",
-      "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg=="
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
     },
     "object-visit": {
       "version": "1.0.1",
@@ -7360,18 +7233,6 @@
         "isobject": "^3.0.0"
       }
     },
-    "object.assign": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
-      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.2",
-        "function-bind": "^1.1.1",
-        "has-symbols": "^1.0.0",
-        "object-keys": "^1.0.11"
-      }
-    },
     "object.entries": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz",
@@ -7442,6 +7303,12 @@
         "wrappy": "1"
       }
     },
+    "opener": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
+      "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
+      "dev": true
+    },
     "opn": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@@ -7561,15 +7428,15 @@
       "dev": true
     },
     "p-is-promise": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz",
-      "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+      "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
       "dev": true
     },
     "p-limit": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
-      "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
+      "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
       "dev": true,
       "requires": {
         "p-try": "^2.0.0"
@@ -7597,9 +7464,9 @@
       "dev": true
     },
     "p-try": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
-      "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
       "dev": true
     },
     "pako": {
@@ -7670,9 +7537,9 @@
       "dev": true
     },
     "parseurl": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
-      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
       "dev": true
     },
     "pascalcase": {
@@ -7868,6 +7735,25 @@
       "requires": {
         "postcss": "^7.0.2",
         "postcss-selector-parser": "^5.0.0"
+      },
+      "dependencies": {
+        "cssesc": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
+          "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
+          "dev": true
+        },
+        "postcss-selector-parser": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
+          "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+          "dev": true,
+          "requires": {
+            "cssesc": "^2.0.0",
+            "indexes-of": "^1.0.1",
+            "uniq": "^1.0.1"
+          }
+        }
       }
     },
     "postcss-cli": {
@@ -7977,6 +7863,25 @@
       "requires": {
         "postcss": "^7.0.2",
         "postcss-selector-parser": "^5.0.0-rc.3"
+      },
+      "dependencies": {
+        "cssesc": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
+          "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
+          "dev": true
+        },
+        "postcss-selector-parser": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
+          "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+          "dev": true,
+          "requires": {
+            "cssesc": "^2.0.0",
+            "indexes-of": "^1.0.1",
+            "uniq": "^1.0.1"
+          }
+        }
       }
     },
     "postcss-dir-pseudo-class": {
@@ -7987,6 +7892,25 @@
       "requires": {
         "postcss": "^7.0.2",
         "postcss-selector-parser": "^5.0.0-rc.3"
+      },
+      "dependencies": {
+        "cssesc": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
+          "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
+          "dev": true
+        },
+        "postcss-selector-parser": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
+          "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+          "dev": true,
+          "requires": {
+            "cssesc": "^2.0.0",
+            "indexes-of": "^1.0.1",
+            "uniq": "^1.0.1"
+          }
+        }
       }
     },
     "postcss-double-position-gradients": {
@@ -8177,19 +8101,6 @@
         "postcss": "^7.0.6",
         "postcss-selector-parser": "^6.0.0",
         "postcss-value-parser": "^3.3.1"
-      },
-      "dependencies": {
-        "postcss-selector-parser": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
-          "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
-          "dev": true,
-          "requires": {
-            "cssesc": "^3.0.0",
-            "indexes-of": "^1.0.1",
-            "uniq": "^1.0.1"
-          }
-        }
       }
     },
     "postcss-modules-scope": {
@@ -8200,19 +8111,6 @@
       "requires": {
         "postcss": "^7.0.6",
         "postcss-selector-parser": "^6.0.0"
-      },
-      "dependencies": {
-        "postcss-selector-parser": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
-          "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
-          "dev": true,
-          "requires": {
-            "cssesc": "^3.0.0",
-            "indexes-of": "^1.0.1",
-            "uniq": "^1.0.1"
-          }
-        }
       }
     },
     "postcss-modules-values": {
@@ -8315,6 +8213,25 @@
       "requires": {
         "postcss": "^7.0.2",
         "postcss-selector-parser": "^5.0.0-rc.3"
+      },
+      "dependencies": {
+        "cssesc": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
+          "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
+          "dev": true
+        },
+        "postcss-selector-parser": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
+          "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+          "dev": true,
+          "requires": {
+            "cssesc": "^2.0.0",
+            "indexes-of": "^1.0.1",
+            "uniq": "^1.0.1"
+          }
+        }
       }
     },
     "postcss-replace-overflow-wrap": {
@@ -8393,22 +8310,14 @@
       }
     },
     "postcss-selector-parser": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
-      "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
+      "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
       "dev": true,
       "requires": {
-        "cssesc": "^2.0.0",
+        "cssesc": "^3.0.0",
         "indexes-of": "^1.0.1",
         "uniq": "^1.0.1"
-      },
-      "dependencies": {
-        "cssesc": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
-          "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
-          "dev": true
-        }
       }
     },
     "postcss-syntax": {
@@ -8450,14 +8359,6 @@
         "ansi-regex": "^4.0.0",
         "ansi-styles": "^3.2.0",
         "react-is": "^16.8.4"
-      },
-      "dependencies": {
-        "react-is": {
-          "version": "16.8.6",
-          "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
-          "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
-          "dev": true
-        }
       }
     },
     "pretty-hrtime": {
@@ -8513,13 +8414,13 @@
       }
     },
     "proxy-addr": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
-      "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
+      "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
       "dev": true,
       "requires": {
         "forwarded": "~0.1.2",
-        "ipaddr.js": "1.8.0"
+        "ipaddr.js": "1.9.0"
       }
     },
     "prr": {
@@ -8683,9 +8584,9 @@
       }
     },
     "react-ace": {
-      "version": "6.4.0",
-      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.4.0.tgz",
-      "integrity": "sha512-woTTgGk9x4GRRWiM4QLNOspjaJAYLX3UZ3J2XRYQvJiN6wyxrFY9x7rdOKc+4Tj+khb/ccPiDj/kll4UeJEDPw==",
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.5.0.tgz",
+      "integrity": "sha512-W8iA6669Tf3sfjCsBg8gKs2pUVMy6BroX6O6GZcgadnLN+MTq7jhs6Q2Rsjq3E3SrWjyA9vZgs1Uzjy8XgWX5w==",
       "requires": {
         "brace": "^0.11.1",
         "diff-match-patch": "^1.0.4",
@@ -8706,9 +8607,9 @@
       }
     },
     "react-is": {
-      "version": "16.8.3",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz",
-      "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA=="
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
+      "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
     },
     "react-lifecycles-compat": {
       "version": "3.0.4",
@@ -8918,9 +8819,9 @@
       }
     },
     "regenerator-runtime": {
-      "version": "0.12.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
-      "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
+      "version": "0.13.2",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
+      "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
     },
     "regex-not": {
       "version": "1.0.2",
@@ -9100,9 +9001,9 @@
       "dev": true
     },
     "require-main-filename": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
-      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
       "dev": true
     },
     "requires-port": {
@@ -9142,6 +9043,32 @@
       "requires": {
         "expand-tilde": "^2.0.0",
         "global-modules": "^1.0.0"
+      },
+      "dependencies": {
+        "global-modules": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+          "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+          "dev": true,
+          "requires": {
+            "global-prefix": "^1.0.1",
+            "is-windows": "^1.0.1",
+            "resolve-dir": "^1.0.0"
+          }
+        },
+        "global-prefix": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+          "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+          "dev": true,
+          "requires": {
+            "expand-tilde": "^2.0.2",
+            "homedir-polyfill": "^1.0.1",
+            "ini": "^1.3.4",
+            "is-windows": "^1.0.1",
+            "which": "^1.2.14"
+          }
+        }
       }
     },
     "resolve-from": {
@@ -9385,6 +9312,12 @@
             "read-pkg": "^1.0.0"
           }
         },
+        "require-main-filename": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+          "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+          "dev": true
+        },
         "string-width": {
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -9535,9 +9468,9 @@
       }
     },
     "semver": {
-      "version": "5.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
-      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+      "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
       "dev": true
     },
     "send": {
@@ -9579,9 +9512,9 @@
       }
     },
     "serialize-javascript": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz",
-      "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz",
+      "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
       "dev": true
     },
     "serve-index": {
@@ -9942,9 +9875,9 @@
       }
     },
     "source-map-support": {
-      "version": "0.5.11",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.11.tgz",
-      "integrity": "sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ==",
+      "version": "0.5.12",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
+      "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
       "dev": true,
       "requires": {
         "buffer-from": "^1.0.0",
@@ -9995,9 +9928,9 @@
       }
     },
     "spdx-license-ids": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz",
-      "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
+      "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==",
       "dev": true
     },
     "spdx-ranges": {
@@ -10370,12 +10303,6 @@
         "get-stdin": "^4.0.1"
       }
     },
-    "strip-json-comments": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
-      "dev": true
-    },
     "style-loader": {
       "version": "0.23.1",
       "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz",
@@ -10447,12 +10374,6 @@
         "table": "^5.0.0"
       },
       "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-          "dev": true
-        },
         "camelcase": {
           "version": "4.1.0",
           "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
@@ -10506,30 +10427,10 @@
           "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
           "dev": true
         },
-        "global-modules": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
-          "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
-          "dev": true,
-          "requires": {
-            "global-prefix": "^3.0.0"
-          }
-        },
-        "global-prefix": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
-          "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
-          "dev": true,
-          "requires": {
-            "ini": "^1.3.5",
-            "kind-of": "^6.0.2",
-            "which": "^1.3.1"
-          }
-        },
         "ignore": {
-          "version": "5.0.6",
-          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.6.tgz",
-          "integrity": "sha512-/+hp3kUf/Csa32ktIaj0OlRqQxrgs30n62M90UBpNd9k+ENEch5S+hmbW3DtcJGz3sYFTh4F3A6fQ0q7KWsp4w==",
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.1.tgz",
+          "integrity": "sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==",
           "dev": true
         },
         "indent-string": {
@@ -10538,16 +10439,6 @@
           "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
           "dev": true
         },
-        "js-yaml": {
-          "version": "3.13.0",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz",
-          "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==",
-          "dev": true,
-          "requires": {
-            "argparse": "^1.0.7",
-            "esprima": "^4.0.0"
-          }
-        },
         "locate-path": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@@ -10659,15 +10550,6 @@
             "strip-ansi": "^5.1.0"
           }
         },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        },
         "strip-indent": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
@@ -10692,30 +10574,30 @@
       }
     },
     "stylelint-config-recommended": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-2.1.0.tgz",
-      "integrity": "sha512-ajMbivOD7JxdsnlS5945KYhvt7L/HwN6YeYF2BH6kE4UCLJR0YvXMf+2j7nQpJyYLZx9uZzU5G1ZOSBiWAc6yA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-2.2.0.tgz",
+      "integrity": "sha512-bZ+d4RiNEfmoR74KZtCKmsABdBJr4iXRiCso+6LtMJPw5rd/KnxUWTxht7TbafrTJK1YRjNgnN0iVZaJfc3xJA==",
       "dev": true
     },
     "stylelint-config-recommended-scss": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-3.2.0.tgz",
-      "integrity": "sha512-M8BFHMRf8KNz5EQPKJd8nMCGmBd2o5coDEObfHVbEkyLDgjIf1V+U5dHjaGgvhm0zToUxshxN+Gc5wpbOOew4g==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-3.3.0.tgz",
+      "integrity": "sha512-BvuuLYwoet8JutOP7K1a8YaiENN+0HQn390eDi0SWe1h7Uhx6O3GUQ6Ubgie9b/AmHX4Btmp+ZzVGbzriFTBcA==",
       "dev": true,
       "requires": {
-        "stylelint-config-recommended": "^2.0.0"
+        "stylelint-config-recommended": "^2.2.0"
       }
     },
     "stylelint-scss": {
-      "version": "3.5.4",
-      "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.4.tgz",
-      "integrity": "sha512-hEdEOfFXVqxWcUbenBONW/cAw5cJcEDasY8tGwKNAAn1GDHoZO1ATdWpr+iIk325mPGIQqVb1sUxsRxuL70trw==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.6.0.tgz",
+      "integrity": "sha512-Qpw0gl6iLBon5JNeFZjVYOEayd/e+WYIdY2vFhZuXeHC6jb8wl0wRZY97jATt/uxZzdtU3tGLAvJOUMuFp18vw==",
       "dev": true,
       "requires": {
         "lodash": "^4.17.11",
         "postcss-media-query-parser": "^0.2.3",
         "postcss-resolve-nested-selector": "^0.1.1",
-        "postcss-selector-parser": "^5.0.0",
+        "postcss-selector-parser": "^6.0.2",
         "postcss-value-parser": "^3.3.1"
       }
     },
@@ -10806,12 +10688,6 @@
         "string-width": "^3.0.0"
       },
       "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-          "dev": true
-        },
         "string-width": {
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@@ -10822,22 +10698,13 @@
             "is-fullwidth-code-point": "^2.0.0",
             "strip-ansi": "^5.1.0"
           }
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
         }
       }
     },
     "tapable": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz",
-      "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
+      "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
       "dev": true
     },
     "tar": {
@@ -10879,15 +10746,15 @@
       }
     },
     "test-exclude": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.1.0.tgz",
-      "integrity": "sha512-gwf0S2fFsANC55fSeSqpb8BYk6w3FDvwZxfNjeF6FRgvFa43r+7wRiA/Q0IxoRU37wB/LE8IQ4221BsNucTaCA==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.2.tgz",
+      "integrity": "sha512-N2pvaLpT8guUpb5Fe1GJlmvmzH3x+DAKmmyEQmFP792QcLYoGE1syxztSvPD1V8yPe6VrcCt6YGQVjSRjCASsA==",
       "dev": true,
       "requires": {
-        "arrify": "^1.0.1",
+        "glob": "^7.1.3",
         "minimatch": "^3.0.4",
         "read-pkg-up": "^4.0.0",
-        "require-main-filename": "^1.0.1"
+        "require-main-filename": "^2.0.0"
       }
     },
     "throat": {
@@ -11055,6 +10922,12 @@
         "glob": "^7.1.2"
       }
     },
+    "tryer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
+      "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
+      "dev": true
+    },
     "ts-jest": {
       "version": "24.0.2",
       "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.0.2.tgz",
@@ -11099,9 +10972,9 @@
       }
     },
     "ts-loader": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.3.3.tgz",
-      "integrity": "sha512-KwF1SplmOJepnoZ4eRIloH/zXL195F51skt7reEsS6jvDqzgc/YSbz9b8E07GxIUwLXdcD4ssrJu6v8CwaTafA==",
+      "version": "5.4.3",
+      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.3.tgz",
+      "integrity": "sha512-pHwZFkZioL7Yi2su0bhW2/djxZ+0iGat1cxlAif4Eg9j5znVYuWGtW0YYY/5w8W+IzLcAlD5KwJDrs5unUKIRA==",
       "dev": true,
       "requires": {
         "chalk": "^2.3.0",
@@ -11112,9 +10985,9 @@
       }
     },
     "ts-node": {
-      "version": "8.0.3",
-      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.3.tgz",
-      "integrity": "sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.1.0.tgz",
+      "integrity": "sha512-34jpuOrxDuf+O6iW1JpgTRDFynUZ1iEqtYruBqh35gICNjN8x+LpVcPAcwzLPi9VU6mdA3ym+x233nZmZp445A==",
       "dev": true,
       "requires": {
         "arg": "^4.1.0",
@@ -11130,12 +11003,12 @@
       "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
     },
     "tslint": {
-      "version": "5.15.0",
-      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.15.0.tgz",
-      "integrity": "sha512-6bIEujKR21/3nyeoX2uBnE8s+tMXCQXhqMmaIPJpHmXJoBJPTLcI7/VHRtUwMhnLVdwLqqY3zmd8Dxqa5CVdJA==",
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.16.0.tgz",
+      "integrity": "sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA==",
       "dev": true,
       "requires": {
-        "babel-code-frame": "^6.22.0",
+        "@babel/code-frame": "^7.0.0",
         "builtin-modules": "^1.1.1",
         "chalk": "^2.3.0",
         "commander": "^2.12.1",
@@ -11148,18 +11021,6 @@
         "semver": "^5.3.0",
         "tslib": "^1.8.0",
         "tsutils": "^2.29.0"
-      },
-      "dependencies": {
-        "js-yaml": {
-          "version": "3.13.1",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
-          "dev": true,
-          "requires": {
-            "argparse": "^1.0.7",
-            "esprima": "^4.0.0"
-          }
-        }
       }
     },
     "tslint-loader": {
@@ -11245,9 +11106,9 @@
       "dev": true
     },
     "typescript": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
-      "integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==",
+      "version": "3.4.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.4.tgz",
+      "integrity": "sha512-xt5RsIRCEaf6+j9AyOBgvVuAec0i92rgCaS3S+UVf5Z/vF2Hvtsw08wtUTJqp4djwznoAgjSxeCcU4r+CcDBJA==",
       "dev": true
     },
     "ua-parser-js": {
@@ -11256,13 +11117,13 @@
       "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
     },
     "uglify-js": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.3.tgz",
-      "integrity": "sha512-rIQPT2UMDnk4jRX+w4WO84/pebU2jiLsjgIyrCktYgSvx28enOE3iYQMr+BD1rHiitWnDmpu0cY/LfIEpKcjcw==",
+      "version": "3.5.6",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.6.tgz",
+      "integrity": "sha512-YDKRX8F0Y+Jr7LhoVk0n4G7ltR3Y7qFAj+DtVBthlOgCcIj1hyMigCfousVfn9HKmvJ+qiFlLDwaHx44/e5ZKw==",
       "dev": true,
       "optional": true,
       "requires": {
-        "commander": "~2.19.0",
+        "commander": "~2.20.0",
         "source-map": "~0.6.1"
       }
     },
@@ -11458,9 +11319,9 @@
       }
     },
     "upath": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
-      "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
+      "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
       "dev": true
     },
     "uri-js": {
@@ -11497,9 +11358,9 @@
       }
     },
     "url-parse": {
-      "version": "1.4.5",
-      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.5.tgz",
-      "integrity": "sha512-4XDvC5vZRjEpjP0L4znrWeoH8P8F0XGBlfLdABi/6oV4o8xUVbTpyrxWHxkK2bT0pSIpcjdIzSoWUhlUfawCAQ==",
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.6.tgz",
+      "integrity": "sha512-/B8AD9iQ01seoXmXf9z/MjLZQIdOoYl/+gvsQF6+mpnxaTfG9P7srYaiqaDMyKkR36XMXfhqSHss5MyFAO8lew==",
       "dev": true,
       "requires": {
         "querystringify": "^2.0.0",
@@ -11690,9 +11551,9 @@
       "dev": true
     },
     "webpack": {
-      "version": "4.29.6",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.29.6.tgz",
-      "integrity": "sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw==",
+      "version": "4.30.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.30.0.tgz",
+      "integrity": "sha512-4hgvO2YbAFUhyTdlR4FNyt2+YaYBYHavyzjCMbZzgglo02rlKi/pcsEzwCuCpsn1ryzIl1cq/u8ArIKu8JBYMg==",
       "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.8.5",
@@ -11729,10 +11590,48 @@
         }
       }
     },
+    "webpack-bundle-analyzer": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.3.2.tgz",
+      "integrity": "sha512-7qvJLPKB4rRWZGjVp5U1KEjwutbDHSKboAl0IfafnrdXMrgC0tOtZbQD6Rw0u4cmpgRN4O02Fc0t8eAT+FgGzA==",
+      "dev": true,
+      "requires": {
+        "acorn": "^6.0.7",
+        "acorn-walk": "^6.1.1",
+        "bfj": "^6.1.1",
+        "chalk": "^2.4.1",
+        "commander": "^2.18.0",
+        "ejs": "^2.6.1",
+        "express": "^4.16.3",
+        "filesize": "^3.6.1",
+        "gzip-size": "^5.0.0",
+        "lodash": "^4.17.10",
+        "mkdirp": "^0.5.1",
+        "opener": "^1.5.1",
+        "ws": "^6.0.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "6.1.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
+          "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
+          "dev": true
+        },
+        "ws": {
+          "version": "6.2.1",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
+          "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+          "dev": true,
+          "requires": {
+            "async-limiter": "~1.0.0"
+          }
+        }
+      }
+    },
     "webpack-cli": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.0.tgz",
-      "integrity": "sha512-t1M7G4z5FhHKJ92WRKwZ1rtvi7rHc0NZoZRbSkol0YKl4HvcC8+DsmGDmK7MmZxHSAetHagiOsjOB6MmzC2TUw==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.1.tgz",
+      "integrity": "sha512-c2inFU7SM0IttEgF7fK6AaUsbBnORRzminvbyRKS+NlbQHVZdCtzKBlavRL5359bFsywXGRAItA5di/IruC8mg==",
       "dev": true,
       "requires": {
         "chalk": "^2.4.1",
@@ -11748,6 +11647,30 @@
         "yargs": "^12.0.5"
       },
       "dependencies": {
+        "global-modules": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+          "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+          "dev": true,
+          "requires": {
+            "global-prefix": "^1.0.1",
+            "is-windows": "^1.0.1",
+            "resolve-dir": "^1.0.0"
+          }
+        },
+        "global-prefix": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+          "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+          "dev": true,
+          "requires": {
+            "expand-tilde": "^2.0.2",
+            "homedir-polyfill": "^1.0.1",
+            "ini": "^1.3.4",
+            "is-windows": "^1.0.1",
+            "which": "^1.2.14"
+          }
+        },
         "supports-color": {
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -11823,26 +11746,6 @@
           "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
           "dev": true
         },
-        "chokidar": {
-          "version": "2.1.5",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
-          "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
-          "dev": true,
-          "requires": {
-            "anymatch": "^2.0.0",
-            "async-each": "^1.0.1",
-            "braces": "^2.3.2",
-            "fsevents": "^1.2.7",
-            "glob-parent": "^3.1.0",
-            "inherits": "^2.0.3",
-            "is-binary-path": "^1.0.0",
-            "is-glob": "^4.0.0",
-            "normalize-path": "^3.0.0",
-            "path-is-absolute": "^1.0.0",
-            "readdirp": "^2.2.1",
-            "upath": "^1.1.1"
-          }
-        },
         "debug": {
           "version": "4.1.1",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -11852,12 +11755,6 @@
             "ms": "^2.1.1"
           }
         },
-        "normalize-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-          "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-          "dev": true
-        },
         "semver": {
           "version": "6.0.0",
           "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
@@ -11872,12 +11769,6 @@
           "requires": {
             "ansi-regex": "^2.0.0"
           }
-        },
-        "upath": {
-          "version": "1.1.2",
-          "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
-          "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
-          "dev": true
         }
       }
     },
@@ -12117,6 +12008,14 @@
         "which-module": "^2.0.0",
         "y18n": "^3.2.1 || ^4.0.0",
         "yargs-parser": "^11.1.1"
+      },
+      "dependencies": {
+        "require-main-filename": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+          "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+          "dev": true
+        }
       }
     },
     "yargs-parser": {
@@ -12129,21 +12028,10 @@
         "decamelize": "^1.2.0"
       }
     },
-    "yargs-unparser": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz",
-      "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==",
-      "dev": true,
-      "requires": {
-        "flat": "^4.1.0",
-        "lodash": "^4.17.11",
-        "yargs": "^12.0.5"
-      }
-    },
     "yn": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/yn/-/yn-3.0.0.tgz",
-      "integrity": "sha512-+Wo/p5VRfxUgBUGy2j/6KX2mj9AYJWOHuhMjMcbBFc3y54o9/4buK1ksBvuiK01C3kby8DH9lSmJdSxw+4G/2Q==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz",
+      "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==",
       "dev": true
     }
   }
diff --git a/web-console/package.json b/web-console/package.json
index dcd07d4..ad9c0f8 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -8,6 +8,16 @@
     "type": "git",
     "url": "https://github.com/apache/druid/"
   },
+  "jest": {
+    "preset": "ts-jest",
+    "testEnvironment": "jsdom",
+    "moduleNameMapper": {
+      "\\.scss$": "identity-obj-proxy"
+    },
+    "testMatch": [
+      "**/?(*.)+(spec).ts?(x)"
+    ]
+  },
   "scripts": {
     "watch": "./script/watch",
     "compile": "./script/build",
@@ -16,7 +26,7 @@
     "test": "jest --silent 2>&1",
     "tslint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter 'src/**/*.ts?(x)'",
     "tslint-fix": "npm run tslint -- --fix",
-    "tslint-changed-only": "git diff --diff-filter=ACMR --name-only | grep -E \\.tsx\\?$ | xargs ./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter",
+    "tslint-changed-only": "git diff --diff-filter=ACMR --cached --name-only | grep -E \\.tsx\\?$ | xargs ./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter",
     "tslint-fix-changed-only": "npm run tslint-changed-only -- --fix",
     "generate-licenses-file": "license-checker --production --json --out licenses.json",
     "check-licenses": "license-checker --production --onlyAllow 'Apache-1.1;Apache-2.0;BSD-2-Clause;BSD-3-Clause;MIT;CC0-1.0' --summary",
@@ -49,7 +59,6 @@
     "@types/hjson": "^2.4.1",
     "@types/jest": "^24.0.11",
     "@types/lodash.debounce": "^4.0.6",
-    "@types/mocha": "^5.2.6",
     "@types/node": "^11.13.4",
     "@types/numeral": "^0.0.25",
     "@types/react-dom": "^16.8.4",
@@ -62,7 +71,6 @@
     "ignore-styles": "^5.0.1",
     "jest": "^24.7.1",
     "license-checker": "^25.0.1",
-    "mocha": "^6.1.3",
     "node-sass": "^4.11.0",
     "node-sass-chokidar": "^1.3.4",
     "postcss-cli": "^6.1.2",
@@ -81,6 +89,7 @@
     "tslint-loader": "^3.5.4",
     "typescript": "^3.4.3",
     "webpack": "^4.29.6",
+    "webpack-bundle-analyzer": "^3.3.2",
     "webpack-cli": "^3.3.0",
     "webpack-dev-server": "^3.3.1"
   }
diff --git a/web-console/script/mkcomp b/web-console/script/mkcomp
new file mode 100755
index 0000000..5ef5d74
--- /dev/null
+++ b/web-console/script/mkcomp
@@ -0,0 +1,135 @@
+#!/usr/bin/env node
+/*
+ * 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.
+ */
+
+let fs = require('fs-extra');
+
+if (!(process.argv.length === 3 || process.argv.length === 4)) {
+  console.log('Usage: mkcomp <what?> <component-name>');
+  process.exit();
+}
+
+let name;
+let what;
+if (process.argv.length === 4) {
+  what = process.argv[2];
+  name = process.argv[3];
+  if (!(what === 'component' || what === 'dialog' || what === 'singleton')) {
+    console.log(`Bad what, should be on of: component, dialog, singleton`);
+    process.exit();
+  }
+} else {
+  what = 'component';
+  name = process.argv[2];
+}
+
+if (!/^([a-z-])+$/.test(name)) {
+  console.log('must be a hyphen case name');
+  process.exit();
+}
+
+let path = `./src/${what}s/`;
+fs.ensureDirSync(path);
+console.log('Making path:', path);
+
+const camelName = name.replace(/(^|-)[a-z]/g, (s) => s.replace('-', '').toUpperCase());
+const year = (new Date()).getFullYear();
+
+function writeFile(path, data) {
+  try {
+    return fs.writeFileSync(path, data, {
+      flag: 'wx', // x = fail if file exists
+      encoding: 'utf8'
+    });
+  } catch (error) {
+    return console.log(`Skipping ${path}`);
+  }
+}
+
+// Make the TypeScript file
+writeFile(path + name + '.tsx',
+`/*
+ * 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, InputGroup } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import classNames from 'classnames';
+import * as React from 'react';
+
+import './${name}.scss';
+
+export interface ${camelName}Props extends React.Props<any> {
+}
+
+export interface ${camelName}State {
+}
+
+export class ${camelName} extends React.Component<${camelName}Props, ${camelName}State> {
+  constructor(props: ${camelName}Props, context: any) {
+    super(props, context);
+    // this.state = {};
+
+  }
+
+  render() {
+    return <div className="${name}">
+    
+    </div>;
+  }
+}
+`);
+
+// Make the SASS file
+writeFile(path + name + '.scss',
+`/*
+ * 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.
+ */
+
+.${name} {
+
+}
+`);
diff --git a/web-console/src/components/array-input.tsx b/web-console/src/components/array-input.tsx
new file mode 100644
index 0000000..4c6d84c
--- /dev/null
+++ b/web-console/src/components/array-input.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 { InputGroup, ITagInputProps } from '@blueprintjs/core';
+import * as React from 'react';
+
+export interface ArrayInputProps extends ITagInputProps {
+
+}
+
+export class ArrayInput extends React.Component<ArrayInputProps, { stringValue: string }> {
+  constructor(props: ArrayInputProps) {
+    super(props);
+    this.state = {
+      stringValue: Array.isArray(props.values) ? props.values.join(', ') : ''
+    };
+  }
+
+  private handleChange = (e: any) => {
+    const { onChange } = this.props;
+    const stringValue = e.target.value;
+    const newValues = stringValue.split(',').map((v: string) => v.trim());
+    const newValuesFiltered = newValues.filter(Boolean);
+    this.setState({
+      stringValue: newValues.length === newValuesFiltered.length ? newValues.join(', ') : stringValue
+    });
+    if (onChange) onChange(stringValue === '' ? undefined : newValuesFiltered);
+  }
+
+  render() {
+    const { className, placeholder, large, disabled } = this.props;
+    const { stringValue } = this.state;
+    return <InputGroup
+      className={className}
+      value={stringValue}
+      onChange={this.handleChange}
+      placeholder={placeholder}
+      large={large}
+      disabled={disabled}
+    />;
+  }
+}
diff --git a/web-console/src/components/auto-form.scss b/web-console/src/components/auto-form.scss
index 4b772c4..a5b4ca2 100644
--- a/web-console/src/components/auto-form.scss
+++ b/web-console/src/components/auto-form.scss
@@ -20,4 +20,22 @@
   .ace_scroller {
     background-color: #212c36;
   }
+
+  // Popover in info label
+  label.bp3-label {
+    position: relative;
+
+    .bp3-text-muted {
+      position: absolute;
+      right: 0;
+
+      .bp3-popover-wrapper {
+        display: inline;
+
+        .bp3-popover-target {
+          display: inline;
+        }
+      }
+    }
+  }
 }
diff --git a/web-console/src/components/auto-form.tsx b/web-console/src/components/auto-form.tsx
index ac550a0..4add8ea 100644
--- a/web-console/src/components/auto-form.tsx
+++ b/web-console/src/components/auto-form.tsx
@@ -16,35 +16,62 @@
  * limitations under the License.
  */
 
-import { InputGroup } from '@blueprintjs/core';
-import { FormGroup, HTMLSelect, NumericInput, TagInput } from '@blueprintjs/core';
+import {
+  Button,
+  FormGroup,
+  HTMLSelect,
+  Icon,
+  InputGroup,
+  Menu,
+  MenuItem,
+  NumericInput,
+  Popover,
+  Position
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
 import * as React from 'react';
 
+import { deepDelete, deepGet, deepSet } from '../utils/object-change';
+
+import { ArrayInput } from './array-input';
 import { JSONInput } from './json-input';
 
 import './auto-form.scss';
 
-interface Field {
+export interface SuggestionGroup {
+  group: string;
+  suggestions: string[];
+}
+
+export interface Field<T> {
   name: string;
   label?: string;
+  info?: React.ReactNode;
   type: 'number' | 'size-bytes' | 'string' | 'boolean' | 'string-array' | 'json';
+  defaultValue?: any;
+  isDefined?: (model: T) => boolean;
+  disabled?: boolean;
+  suggestions?: (string | SuggestionGroup)[];
+  placeholder?: string;
   min?: number;
 }
 
 export interface AutoFormProps<T> extends React.Props<any> {
-  fields: Field[];
+  fields: Field<T>[];
   model: T | null;
-  onChange: (newValue: T) => void;
+  onChange: (newModel: T) => void;
+  showCustom?: (model: T) => boolean;
   updateJSONValidity?: (jsonValidity: boolean) => void;
+  large?: boolean;
 }
 
 export interface AutoFormState<T> {
   jsonInputsValidity: any;
 }
 
-export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState<T>> {
+export class AutoForm<T extends Record<string, any>> extends React.Component<AutoFormProps<T>, AutoFormState<T>> {
   static makeLabelName(label: string): string {
-    let newLabel = label.split(/(?=[A-Z])/).map(s => s.toLowerCase()).join(' ');
+    let newLabel = label.split(/(?=[A-Z])/).join(' ').toLowerCase().replace(/\./g, ' ');
     newLabel = newLabel[0].toUpperCase() + newLabel.slice(1);
     return newLabel;
   }
@@ -56,57 +83,135 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState
     };
   }
 
-  private renderNumberInput(field: Field): JSX.Element {
-    const { model, onChange } = this.props;
+  private fieldChange = (field: Field<T>, newValue: any) => {
+    const { model } = this.props;
+    if (!model) return;
+    const newModel = typeof newValue === 'undefined' ? deepDelete(model, field.name) : deepSet(model, field.name, newValue);
+    this.modelChange(newModel);
+  }
+
+  private modelChange = (newModel: T) => {
+    const { fields, onChange } = this.props;
+
+    for (const someField of fields) {
+      if (someField.isDefined && !someField.isDefined(newModel)) {
+        newModel = deepDelete(newModel, someField.name);
+      } else if (typeof someField.defaultValue !== 'undefined' && typeof deepGet(newModel, someField.name) === 'undefined') {
+        newModel = deepSet(newModel, someField.name, someField.defaultValue);
+      }
+    }
+
+    onChange(newModel);
+  }
+
+  private renderNumberInput(field: Field<T>): JSX.Element {
+    const { model, large } = this.props;
     return <NumericInput
-      value={(model as any)[field.name]}
-      onValueChange={(v: any) => {
-        if (isNaN(v)) return;
-        onChange(Object.assign({}, model, { [field.name]: v }));
+      value={deepGet(model as any, field.name) || field.defaultValue}
+      onValueChange={(valueAsNumber: number, valueAsString: string) => {
+        if (valueAsString === '') {
+          this.fieldChange(field, undefined);
+          return;
+        }
+        if (isNaN(valueAsNumber)) return;
+        this.fieldChange(field, valueAsNumber);
       }}
       min={field.min || 0}
+      fill
+      large={large}
+      disabled={field.disabled}
+      placeholder={field.placeholder}
     />;
   }
 
-  private renderSizeBytesInput(field: Field): JSX.Element {
-    const { model, onChange } = this.props;
+  private renderSizeBytesInput(field: Field<T>): JSX.Element {
+    const { model, large } = this.props;
     return <NumericInput
-      value={(model as any)[field.name]}
+      value={deepGet(model as any, field.name) || field.defaultValue}
       onValueChange={(v: number) => {
         if (isNaN(v)) return;
-        onChange(Object.assign({}, model, { [field.name]: v }));
+        this.fieldChange(field, v);
       }}
       min={0}
       stepSize={1000}
       majorStepSize={1000000}
+      large={large}
+      disabled={field.disabled}
     />;
   }
 
-  private renderStringInput(field: Field): JSX.Element {
-    const { model, onChange } = this.props;
+  private renderStringInput(field: Field<T>): 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;
+
     return <InputGroup
-      value={(model as any)[field.name]}
-      onChange={(v: any) => {
-        onChange(Object.assign({}, model, { [field.name]: v }));
+      value={deepGet(model as any, field.name) || field.defaultValue || ''}
+      onChange={(e: any) => {
+        const v = e.target.value;
+        this.fieldChange(field, v === '' ? undefined : v);
       }}
+      placeholder={field.placeholder}
+      rightElement={
+        suggestionsMenu &&
+        <Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>
+          <Button icon={IconNames.CARET_DOWN} minimal />
+        </Popover>
+      }
+      large={large}
+      disabled={field.disabled}
     />;
   }
 
-  private renderBooleanInput(field: Field): JSX.Element {
-    const { model, onChange } = this.props;
+  private renderBooleanInput(field: Field<T>): JSX.Element {
+    const { model, large } = this.props;
+    let curValue = deepGet(model as any, field.name);
+    if (curValue == null) curValue = field.defaultValue;
     return <HTMLSelect
-      value={(model as any)[field.name] === true ? 'True' : 'False'}
+      value={curValue === true ? 'True' : 'False'}
       onChange={(e: any) => {
-        onChange(Object.assign({}, model, { [field.name]: e.currentTarget.value === 'True' }));
+        const v = e.currentTarget.value === 'True';
+        this.fieldChange(field, v);
       }}
+      large={large}
+      disabled={field.disabled}
     >
       <option value="True">True</option>
       <option value="False">False</option>
     </HTMLSelect>;
   }
 
-  private renderJSONInput(field: Field): JSX.Element {
-    const { model, onChange,  updateJSONValidity } = this.props;
+  private renderJSONInput(field: Field<T>): JSX.Element {
+    const { model, updateJSONValidity } = this.props;
     const { jsonInputsValidity } = this.state;
 
     const updateInputValidity = (e: any) => {
@@ -121,25 +226,28 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState
     };
 
     return <JSONInput
-      value={(model as any)[field.name]}
-      onChange={(e: any) => onChange(Object.assign({}, model, { [field.name]: e}))}
+      value={deepGet(model as any, field.name)}
+      onChange={(v: any) => this.fieldChange(field, v)}
       updateInputValidity={updateInputValidity}
     />;
   }
 
-  private renderStringArrayInput(field: Field): JSX.Element {
-    const { model, onChange } = this.props;
-    return <TagInput
-      values={(model as any)[field.name] || []}
+  private renderStringArrayInput(field: Field<T>): JSX.Element {
+    const { model, large } = this.props;
+    return <ArrayInput
+      values={deepGet(model as any, field.name) || []}
       onChange={(v: any) => {
-        onChange(Object.assign({}, model, { [field.name]: v }));
+        this.fieldChange(field, v);
       }}
+      placeholder={field.placeholder}
       addOnBlur
       fill
+      large={large}
+      disabled={field.disabled}
     />;
   }
 
-  renderFieldInput(field: Field) {
+  renderFieldInput(field: Field<T>) {
     switch (field.type) {
       case 'number': return this.renderNumberInput(field);
       case 'size-bytes': return this.renderSizeBytesInput(field);
@@ -151,17 +259,45 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState
     }
   }
 
-  renderField(field: Field) {
+  private renderField = (field: Field<T>) => {
+    const { model } = this.props;
+    if (!model) return null;
+    if (field.isDefined && !field.isDefined(model)) return null;
+
     const label = field.label || AutoForm.makeLabelName(field.name);
-    return <FormGroup label={label} key={field.name}>
+    return <FormGroup
+      key={field.name}
+      label={label}
+      labelInfo={
+        field.info &&
+        <Popover
+          content={<div className="label-info-text">{field.info}</div>}
+          position="left-bottom"
+        >
+          <Icon icon={IconNames.INFO_SIGN} iconSize={14}/>
+        </Popover>
+      }
+    >
       {this.renderFieldInput(field)}
     </FormGroup>;
   }
 
+  renderCustom() {
+    const { model } = this.props;
+
+    return <FormGroup label="Custom" key="custom">
+      <JSONInput
+        value={model}
+        onChange={this.modelChange}
+      />
+    </FormGroup>;
+  }
+
   render() {
-    const { fields, model } = this.props;
+    const { fields, model, showCustom } = this.props;
     return <div className="auto-form">
-      {model && fields.map(field => this.renderField(field))}
+      {model && fields.map(this.renderField)}
+      {model && showCustom && showCustom(model) && this.renderCustom()}
     </div>;
   }
 }
diff --git a/web-console/src/components/auto-form.scss b/web-console/src/components/center-message.scss
similarity index 77%
copy from web-console/src/components/auto-form.scss
copy to web-console/src/components/center-message.scss
index 4b772c4..3b07b5c 100644
--- a/web-console/src/components/auto-form.scss
+++ b/web-console/src/components/center-message.scss
@@ -16,8 +16,17 @@
  * limitations under the License.
  */
 
-.auto-form {
-  .ace_scroller {
-    background-color: #212c36;
+.center-message {
+  position: absolute;
+  width: 100%;
+  height: 100% !important;
+  overflow: hidden;
+
+  .center-message-inner {
+    position: absolute;
+    top: 40%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    max-width: 50%;
   }
 }
diff --git a/web-console/src/views/sql-view.scss b/web-console/src/components/center-message.tsx
similarity index 68%
copy from web-console/src/views/sql-view.scss
copy to web-console/src/components/center-message.tsx
index 1cd7ecc..89a83f8 100644
--- a/web-console/src/views/sql-view.scss
+++ b/web-console/src/components/center-message.tsx
@@ -16,30 +16,19 @@
  * limitations under the License.
  */
 
-@import "../variables";
+import * as React from 'react';
 
-.sql-view {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
+import './center-message.scss';
 
-  .sql-control {
-    textarea {
-      width: 100%;
-      min-height: 180px;
-    }
-
-    .buttons {
-      padding: $standard-padding 0;
-    }
-  }
-
-  .ReactTable {
-    flex: 1;
+export interface CenterMessageProps extends React.Props<any> {
+}
 
-    .null-table-cell {
-      font-style: italic;
-    }
+export class CenterMessage extends React.Component<CenterMessageProps, {}> {
+  render() {
+    return <div className="center-message bp3-input">
+      <div className="center-message-inner">
+        {this.props.children}
+      </div>
+    </div>;
   }
 }
-
diff --git a/web-console/src/entry.scss b/web-console/src/components/clearable-input.tsx
similarity index 50%
copy from web-console/src/entry.scss
copy to web-console/src/components/clearable-input.tsx
index 8505642..da4df34 100644
--- a/web-console/src/entry.scss
+++ b/web-console/src/components/clearable-input.tsx
@@ -16,37 +16,28 @@
  * limitations under the License.
  */
 
-@import '../node_modules/normalize.css/normalize';
-@import '../node_modules/@blueprintjs/core/lib/css/blueprint';
-@import '../lib/react-table';
+import { Button, InputGroup } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import classNames from 'classnames';
+import * as React from 'react';
 
-html,
-body {
-  //font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-  height: 100%;
-  overflow: hidden;
-  font-size: 13px;
+export interface ClearableInputProps extends React.Props<any> {
+  className?: string;
+  value: string;
+  onChange: (value: string) => void;
+  placeholder: string;
 }
 
-body {
-  &.bp3-dark {
-    background: rgb(41, 55, 66);
-  }
+export class ClearableInput extends React.Component<ClearableInputProps, {}> {
+  render() {
+    const { className, value, onChange, placeholder } = this.props;
 
-  &.mouse-mode {
-    *:focus {
-      outline: none !important;
-    }
+    return <InputGroup
+      className={classNames('clearable-input', className)}
+      value={value}
+      onChange={(e: any) => onChange(e.target.value)}
+      rightElement={value ? <Button icon={IconNames.CROSS} minimal onClick={() => onChange('')} /> : undefined}
+      placeholder={placeholder}
+    />;
   }
 }
-
-svg {
-  width: auto;
-  height: auto;
-}
-
-.app-container {
-  position: absolute;
-  height: 100%;
-  width: 100%;
-}
diff --git a/web-console/src/views/sql-view.scss b/web-console/src/components/external-link.tsx
similarity index 71%
copy from web-console/src/views/sql-view.scss
copy to web-console/src/components/external-link.tsx
index 1cd7ecc..0a9c102 100644
--- a/web-console/src/views/sql-view.scss
+++ b/web-console/src/components/external-link.tsx
@@ -16,30 +16,21 @@
  * limitations under the License.
  */
 
-@import "../variables";
+import * as React from 'react';
 
-.sql-view {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-
-  .sql-control {
-    textarea {
-      width: 100%;
-      min-height: 180px;
-    }
-
-    .buttons {
-      padding: $standard-padding 0;
-    }
-  }
+export interface ExternalLinkProps extends React.Props<any> {
+  href: string;
+}
 
-  .ReactTable {
-    flex: 1;
+export class ExternalLink extends React.Component<ExternalLinkProps, {}> {
+  render() {
+    const { href, children } = this.props;
 
-    .null-table-cell {
-      font-style: italic;
-    }
+    return <a
+      href={href}
+      target="_blank"
+    >
+      {children}
+    </a>;
   }
 }
-
diff --git a/web-console/src/components/header-bar.scss b/web-console/src/components/header-bar.scss
index a35d9f1..77360c7 100644
--- a/web-console/src/components/header-bar.scss
+++ b/web-console/src/components/header-bar.scss
@@ -33,12 +33,7 @@
     }
   }
 
-  .config-popover .bp3-popover-content,
-  .legacy-popover .bp3-popover-content {
-    width: 240px;
-  }
-
-  .help-popover .bp3-popover-content {
-    width: 180px;
+  * {
+    white-space: nowrap;
   }
 }
diff --git a/web-console/src/components/header-bar.tsx b/web-console/src/components/header-bar.tsx
index 0ff5a71..538f8a3 100644
--- a/web-console/src/components/header-bar.tsx
+++ b/web-console/src/components/header-bar.tsx
@@ -16,7 +16,19 @@
  * limitations under the License.
  */
 
-import { Alignment, AnchorButton, Button, Classes, Menu, MenuItem, Navbar, NavbarDivider, NavbarGroup, Popover, Position } from '@blueprintjs/core';
+import {
+  Alignment,
+  AnchorButton,
+  Button, Intent,
+  Menu,
+  MenuDivider,
+  MenuItem,
+  Navbar,
+  NavbarDivider,
+  NavbarGroup,
+  Popover,
+  Position
+} from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import * as React from 'react';
@@ -24,6 +36,7 @@ import * as React from 'react';
 import { AboutDialog } from '../dialogs/about-dialog';
 import { CoordinatorDynamicConfigDialog } from '../dialogs/coordinator-dynamic-config';
 import { OverlordDynamicConfigDialog } from '../dialogs/overlord-dynamic-config';
+import { getWikipediaSpec } from '../utils/example-ingestion-spec';
 import {
   DRUID_DOCS,
   DRUID_GITHUB,
@@ -31,14 +44,16 @@ import {
   LEGACY_COORDINATOR_CONSOLE,
   LEGACY_OVERLORD_CONSOLE
 } from '../variables';
+import { LoadDataViewSeed } from '../views/load-data-view';
 
 import './header-bar.scss';
 
-export type HeaderActiveTab = null | 'datasources' | 'segments' | 'tasks' | 'servers' | 'sql' | 'lookups';
+export type HeaderActiveTab = null | 'load-data' | 'query' | 'datasources' | 'segments' | 'tasks' | 'servers' | 'lookups';
 
 export interface HeaderBarProps extends React.Props<any> {
   active: HeaderActiveTab;
   hideLegacy: boolean;
+  goToLoadDataView: (loadDataViewSeed: LoadDataViewSeed) => void;
 }
 
 export interface HeaderBarState {
@@ -110,6 +125,8 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
     const { active, hideLegacy } = this.props;
     const { aboutDialogOpen, coordinatorDynamicConfigDialogOpen, overlordDynamicConfigDialogOpen } = this.state;
 
+    const loadDataPrimary = false;
+
     const legacyMenu = <Menu>
       <MenuItem icon={IconNames.GRAPH} text="Legacy coordinator console" href={LEGACY_COORDINATOR_CONSOLE} target="_blank" />
       <MenuItem icon={IconNames.MAP} text="Legacy overlord console" href={LEGACY_OVERLORD_CONSOLE} target="_blank" />
@@ -123,7 +140,7 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
     </Menu>;
 
     const configMenu = <Menu>
-      <MenuItem icon={IconNames.COG} text="Coordinator dynamic config" onClick={() => this.setState({ coordinatorDynamicConfigDialogOpen: true })}/>
+      <MenuItem icon={IconNames.SETTINGS} text="Coordinator dynamic config" onClick={() => this.setState({ coordinatorDynamicConfigDialogOpen: true })}/>
       <MenuItem icon={IconNames.WRENCH} text="Overlord dynamic config" onClick={() => this.setState({ overlordDynamicConfigDialogOpen: true })}/>
       <MenuItem icon={IconNames.PROPERTIES} active={active === 'lookups'} text="Lookups" href="#lookups"/>
     </Menu>;
@@ -133,26 +150,39 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
         <a href="#">
           {this.renderLogo()}
         </a>
-        <NavbarDivider />
+
+        <NavbarDivider/>
+        <AnchorButton
+          icon={IconNames.CLOUD_UPLOAD}
+          text="Load data"
+          active={active === 'load-data'}
+          href="#load-data"
+          minimal={!loadDataPrimary}
+          intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
+        />
+        <AnchorButton minimal active={active === 'query'} icon={IconNames.APPLICATION} text="Query" href="#query" />
+
+        <NavbarDivider/>
         <AnchorButton minimal active={active === 'datasources'} icon={IconNames.MULTI_SELECT} text="Datasources" href="#datasources" />
         <AnchorButton minimal active={active === 'segments'} icon={IconNames.STACKED_CHART} text="Segments" href="#segments" />
         <AnchorButton minimal active={active === 'tasks'} icon={IconNames.GANTT_CHART} text="Tasks" href="#tasks" />
+
+        <NavbarDivider/>
         <AnchorButton minimal active={active === 'servers'} icon={IconNames.DATABASE} text="Data servers" href="#servers" />
-        <NavbarDivider />
-        <AnchorButton minimal active={active === 'sql'} icon={IconNames.APPLICATION} text="SQL" href="#sql" />
-        <Popover className="config-popover" content={configMenu} position={Position.BOTTOM_LEFT}>
-          <Button className={Classes.MINIMAL} icon={IconNames.SETTINGS} text="Config"/>
-        </Popover>
+
       </NavbarGroup>
       <NavbarGroup align={Alignment.RIGHT}>
         {
           !hideLegacy &&
-          <Popover className="legacy-popover" content={legacyMenu} position={Position.BOTTOM_RIGHT}>
-            <Button className={Classes.MINIMAL} icon={IconNames.SHARE} text="Legacy"/>
+          <Popover content={legacyMenu} position={Position.BOTTOM_RIGHT}>
+            <Button minimal icon={IconNames.SHARE} text="Legacy"/>
           </Popover>
         }
-        <Popover className="help-popover" content={helpMenu} position={Position.BOTTOM_RIGHT}>
-          <Button className={Classes.MINIMAL} icon={IconNames.HELP} text="Help" />
+        <Popover content={configMenu} position={Position.BOTTOM_RIGHT}>
+          <Button minimal icon={IconNames.COG}/>
+        </Popover>
+        <Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
+          <Button minimal icon={IconNames.HELP}/>
         </Popover>
       </NavbarGroup>
       {
diff --git a/web-console/src/components/auto-form.scss b/web-console/src/components/null-table-cell.scss
similarity index 85%
copy from web-console/src/components/auto-form.scss
copy to web-console/src/components/null-table-cell.scss
index 4b772c4..fd5c50c 100644
--- a/web-console/src/components/auto-form.scss
+++ b/web-console/src/components/null-table-cell.scss
@@ -16,8 +16,16 @@
  * limitations under the License.
  */
 
-.auto-form {
-  .ace_scroller {
-    background-color: #212c36;
+.null-table-cell {
+  &.null {
+    font-style: italic;
+  }
+
+  &.unparseable {
+    color: #9E2B0E;
+  }
+
+  &.timestamp {
+    font-weight: bold;
   }
 }
diff --git a/web-console/src/entry.scss b/web-console/src/components/null-table-cell.tsx
similarity index 52%
copy from web-console/src/entry.scss
copy to web-console/src/components/null-table-cell.tsx
index 8505642..01eeb7d 100644
--- a/web-console/src/entry.scss
+++ b/web-console/src/components/null-table-cell.tsx
@@ -16,37 +16,29 @@
  * limitations under the License.
  */
 
-@import '../node_modules/normalize.css/normalize';
-@import '../node_modules/@blueprintjs/core/lib/css/blueprint';
-@import '../lib/react-table';
+import * as React from 'react';
 
-html,
-body {
-  //font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-  height: 100%;
-  overflow: hidden;
-  font-size: 13px;
-}
+import './null-table-cell.scss';
 
-body {
-  &.bp3-dark {
-    background: rgb(41, 55, 66);
-  }
+export interface NullTableCellProps extends React.Props<any> {
+  value?: any;
+  timestamp?: boolean;
+  unparseable?: boolean;
+}
 
-  &.mouse-mode {
-    *:focus {
-      outline: none !important;
+export class NullTableCell extends React.Component<NullTableCellProps, {}> {
+  render() {
+    const { value, timestamp, unparseable } = this.props;
+    if (unparseable) {
+      return <span className="null-table-cell unparseable">{`error`}</span>;
+    } else if (value !== '' && value != null) {
+      if (timestamp) {
+        return <span className="null-table-cell timestamp" title={value}>{new Date(value).toISOString()}</span>;
+      } else {
+        return value;
+      }
+    } else {
+      return <span className="null-table-cell null">null</span>;
     }
   }
 }
-
-svg {
-  width: auto;
-  height: auto;
-}
-
-.app-container {
-  position: absolute;
-  height: 100%;
-  width: 100%;
-}
diff --git a/web-console/src/components/sql-control.tsx b/web-console/src/components/sql-control.tsx
index 832ea8d..d33f281 100644
--- a/web-console/src/components/sql-control.tsx
+++ b/web-console/src/components/sql-control.tsx
@@ -40,6 +40,7 @@ import * as ReactDOMServer from 'react-dom/server';
 
 import { SQLFunctionDoc } from '../../lib/sql-function-doc';
 import { AppToaster } from '../singletons/toaster';
+import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../variables';
 
 import { MenuCheckbox } from './menu-checkbox';
 
@@ -224,6 +225,12 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
     const { query, autoComplete, bypassCache, wrapQuery } = this.state;
 
     return <Menu>
+      <MenuItem
+        icon={IconNames.HELP}
+        text="Docs"
+        href={isRune ? DRUID_DOCS_RUNE : DRUID_DOCS_SQL}
+        target="_blank"
+      />
       {
         !isRune &&
         <>
@@ -233,15 +240,15 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
             onClick={() => onExplain(query)}
           />
           <MenuCheckbox
-            checked={autoComplete}
-            label="Auto complete"
-            onChange={() => this.setState({autoComplete: !autoComplete})}
-          />
-          <MenuCheckbox
             checked={wrapQuery}
             label="Wrap query with limit"
             onChange={() => this.setState({wrapQuery: !wrapQuery})}
           />
+          <MenuCheckbox
+            checked={autoComplete}
+            label="Auto complete"
+            onChange={() => this.setState({autoComplete: !autoComplete})}
+          />
         </>
       }
       <MenuCheckbox
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index dad1b56..8691939 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -23,14 +23,16 @@ import * as classNames from 'classnames';
 import * as React from 'react';
 import { HashRouter, Route, Switch } from 'react-router-dom';
 
+import { ExternalLink } from './components/external-link';
 import { HeaderActiveTab, HeaderBar } from './components/header-bar';
-import {Loader} from './components/loader';
+import { Loader } from './components/loader';
 import { AppToaster } from './singletons/toaster';
 import { UrlBaser } from './singletons/url-baser';
-import {QueryManager} from './utils';
-import {DRUID_DOCS_API, DRUID_DOCS_SQL, LEGACY_COORDINATOR_CONSOLE, LEGACY_OVERLORD_CONSOLE} from './variables';
+import { QueryManager } from './utils';
+import { DRUID_DOCS_API, DRUID_DOCS_SQL } from './variables';
 import { DatasourcesView } from './views/datasource-view';
 import { HomeView } from './views/home-view';
+import { LoadDataView, LoadDataViewSeed } from './views/load-data-view';
 import { LookupsView } from './views/lookups-view';
 import { SegmentsView } from './views/segments-view';
 import { ServersView } from './views/servers-view';
@@ -80,8 +82,8 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
     if (capabilities === 'working-without-sql') {
       message = <>
         It appears that the SQL endpoint is disabled. The console will fall back
-        to <a href={DRUID_DOCS_API} target="_blank">native Druid APIs</a> and will be
-        limited in functionality. Look at <a href={DRUID_DOCS_SQL} target="_blank">the SQL docs</a> to
+        to <ExternalLink href={DRUID_DOCS_API}>native Druid APIs</ExternalLink> and will be
+        limited in functionality. Look at <ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink> to
         enable the SQL endpoint.
       </>;
     } else if (capabilities === 'broken') {
@@ -98,6 +100,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
     });
   }
 
+  private loadDataViewSeed: LoadDataViewSeed | null;
   private taskId: string | null;
   private datasource: string | null;
   private onlyUnavailable: boolean | null;
@@ -145,8 +148,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
     this.capabilitiesQueryManager.terminate();
   }
 
-  private resetInitialsDelay() {
+  private resetInitialsWithDelay() {
     setTimeout(() => {
+      this.loadDataViewSeed = null;
       this.taskId = null;
       this.datasource = null;
       this.onlyUnavailable = null;
@@ -155,41 +159,85 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
     }, 50);
   }
 
-  private goToTask = (taskId: string) => {
+  private goToLoadDataView = (loadDataViewSeed?: LoadDataViewSeed) => {
+    if (loadDataViewSeed) this.loadDataViewSeed = loadDataViewSeed;
+    window.location.hash = 'load-data';
+    this.resetInitialsWithDelay();
+  }
+
+  private goToTask = (taskId: string | null) => {
     this.taskId = taskId;
     window.location.hash = 'tasks';
-    this.resetInitialsDelay();
+    this.resetInitialsWithDelay();
   }
 
   private goToSegments = (datasource: string, onlyUnavailable = false) => {
     this.datasource = `"${datasource}"`;
     this.onlyUnavailable = onlyUnavailable;
     window.location.hash = 'segments';
-    this.resetInitialsDelay();
+    this.resetInitialsWithDelay();
   }
 
   private goToMiddleManager = (middleManager: string) => {
     this.middleManager = middleManager;
     window.location.hash = 'servers';
-    this.resetInitialsDelay();
+    this.resetInitialsWithDelay();
   }
 
   private goToSql = (initSql: string) => {
     this.initSql = initSql;
-    window.location.hash = 'sql';
-    this.resetInitialsDelay();
+    window.location.hash = 'query';
+    this.resetInitialsWithDelay();
   }
 
-  render() {
+  private wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
     const { hideLegacy } = this.props;
-    const { noSqlMode, capabilitiesLoading } = this.state;
 
-    const wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
-      return <>
-        <HeaderBar active={active} hideLegacy={hideLegacy}/>
-        <div className={classNames('view-container', { scrollable })}>{el}</div>
-      </>;
-    };
+    return <>
+      <HeaderBar active={active} hideLegacy={hideLegacy} goToLoadDataView={this.goToLoadDataView}/>
+      <div className={classNames('view-container', { scrollable })}>{el}</div>
+    </>;
+  }
+
+  private wrappedHomeView = () => {
+    const { noSqlMode } = this.state;
+    return this.wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
+  }
+
+  private wrappedLoadDataView = () => {
+  return this.wrapInViewContainer('load-data', <LoadDataView seed={this.loadDataViewSeed} goToTask={this.goToTask}/>);
+  }
+
+  private wrappedSqlView = () => {
+    return this.wrapInViewContainer('query', <SqlView initSql={this.initSql}/>);
+  }
+
+  private wrappedDatasourcesView = () => {
+    const { noSqlMode } = this.state;
+    return this.wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
+  }
+
+  private wrappedSegmentsView = () => {
+    const { noSqlMode } = this.state;
+    return this.wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
+  }
+
+  private wrappedTasksView = () => {
+    const { noSqlMode } = this.state;
+    return this.wrapInViewContainer('tasks', <TasksView taskId={this.taskId} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} goToLoadDataView={this.goToLoadDataView} noSqlMode={noSqlMode}/>, true);
+  }
+
+  private wrappedServersView = () => {
+    const { noSqlMode } = this.state;
+    return this.wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>, true);
+  }
+
+  private wrappedLookupsView = () => {
+    return this.wrapInViewContainer('lookups', <LookupsView/>);
+  }
+
+  render() {
+    const { capabilitiesLoading } = this.state;
 
     if (capabilitiesLoading) {
       return <div className={'loading-capabilities'}>
@@ -203,47 +251,17 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
     return <HashRouter hashType="noslash">
       <div className="console-application">
         <Switch>
-          <Route
-            path="/datasources"
-            component={() => {
-              return wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
-            }}
-          />
-          <Route
-            path="/segments"
-            component={() => {
-              return wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
-            }}
-          />
-          <Route
-            path="/tasks"
-            component={() => {
-              return wrapInViewContainer('tasks', <TasksView taskId={this.taskId} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} noSqlMode={noSqlMode}/>, true);
-            }}
-          />
-          <Route
-            path="/servers"
-            component={() => {
-              return wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>, true);
-            }}
-          />
-          <Route
-            path="/sql"
-            component={() => {
-              return wrapInViewContainer('sql', <SqlView initSql={this.initSql}/>);
-            }}
-          />
-          <Route
-            path="/lookups"
-            component={() => {
-              return wrapInViewContainer('lookups', <LookupsView />);
-            }}
-          />
-          <Route
-            component={() => {
-              return wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
-            }}
-          />
+          <Route path="/load-data" component={this.wrappedLoadDataView}/>
+          <Route path="/query" component={this.wrappedSqlView}/>
+          <Route path="/sql" component={this.wrappedSqlView}/>
+
+          <Route path="/datasources" component={this.wrappedDatasourcesView}/>
+          <Route path="/segments" component={this.wrappedSegmentsView}/>
+          <Route path="/tasks" component={this.wrappedTasksView}/>
+          <Route path="/servers" component={this.wrappedServersView}/>
+
+          <Route path="/lookups" component={this.wrappedLookupsView}/>
+          <Route component={this.wrappedHomeView}/>
         </Switch>
       </div>
     </HashRouter>;
diff --git a/web-console/src/dialogs/about-dialog.tsx b/web-console/src/dialogs/about-dialog.tsx
index 89272cd..10992a7 100644
--- a/web-console/src/dialogs/about-dialog.tsx
+++ b/web-console/src/dialogs/about-dialog.tsx
@@ -20,6 +20,7 @@ import { AnchorButton, Button, Classes, Dialog, Intent } from '@blueprintjs/core
 import { IconNames } from '@blueprintjs/icons';
 import * as React from 'react';
 
+import { ExternalLink } from '../components/external-link';
 import { DRUID_COMMUNITY, DRUID_DEVELOPER_GROUP, DRUID_USER_GROUP, DRUID_WEBSITE } from '../variables';
 
 export interface AboutDialogProps extends React.Props<any> {
@@ -53,14 +54,14 @@ export class AboutDialog extends React.Component<AboutDialogProps, AboutDialogSt
           </strong>
         </p>
         <p>
-          For help and support with Druid, please refer to the <a
-          href={DRUID_COMMUNITY} target="_blank">community page</a> and the <a
-          href={DRUID_USER_GROUP} target="_blank">user groups</a>.
+          For help and support with Druid, please refer to the <ExternalLink
+          href={DRUID_COMMUNITY}>community page</ExternalLink> and the <ExternalLink
+          href={DRUID_USER_GROUP}>user groups</ExternalLink>.
         </p>
         <p>
           Druid is made with ❤️ by a community of passionate developers.
-          To contribute, join in the discussion on the <a
-          href={DRUID_DEVELOPER_GROUP} target="_blank">developer group</a>.
+          To contribute, join in the discussion on the <ExternalLink
+          href={DRUID_DEVELOPER_GROUP}>developer group</ExternalLink>.
         </p>
       </div>
       <div className={Classes.DIALOG_FOOTER}>
diff --git a/web-console/src/dialogs/async-action-dialog.tsx b/web-console/src/dialogs/async-action-dialog.tsx
index 1d7addb..c29d058 100644
--- a/web-console/src/dialogs/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog.tsx
@@ -66,16 +66,16 @@ export class AsyncActionDialog extends React.Component<AsyncAlertDialogProps, As
         message: `${failText}: ${e.message}`,
         intent: Intent.DANGER
       });
-      onClose(false);
       this.setState({ working: false });
+      onClose(false);
       return;
     }
     AppToaster.show({
       message: successText,
       intent: Intent.SUCCESS
     });
-    onClose(true);
     this.setState({ working: false });
+    onClose(true);
   }
 
   render() {
diff --git a/web-console/src/dialogs/coordinator-dynamic-config.tsx b/web-console/src/dialogs/coordinator-dynamic-config.tsx
index 79461a5..a5c0348 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config.tsx
+++ b/web-console/src/dialogs/coordinator-dynamic-config.tsx
@@ -22,6 +22,7 @@ import axios from 'axios';
 import * as React from 'react';
 
 import { AutoForm } from '../components/auto-form';
+import { ExternalLink } from '../components/external-link';
 import { AppToaster } from '../singletons/toaster';
 import { getDruidErrorMessage, QueryManager } from '../utils';
 
@@ -124,7 +125,7 @@ export class CoordinatorDynamicConfigDialog extends React.Component<CoordinatorD
     >
       <p>
         Edit the coordinator dynamic configuration on the fly.
-        For more information please refer to the <a href="http://druid.io/docs/latest/configuration/index.html#dynamic-configuration" target="_blank">documentation</a>.
+        For more information please refer to the <ExternalLink href="http://druid.io/docs/latest/configuration/index.html#dynamic-configuration">documentation</ExternalLink>.
       </p>
       <AutoForm
         fields={[
diff --git a/web-console/src/dialogs/overlord-dynamic-config.tsx b/web-console/src/dialogs/overlord-dynamic-config.tsx
index eda581e..d22cde0 100644
--- a/web-console/src/dialogs/overlord-dynamic-config.tsx
+++ b/web-console/src/dialogs/overlord-dynamic-config.tsx
@@ -22,6 +22,7 @@ import axios from 'axios';
 import * as React from 'react';
 
 import { AutoForm } from '../components/auto-form';
+import { ExternalLink } from '../components/external-link';
 import { AppToaster } from '../singletons/toaster';
 import { getDruidErrorMessage, QueryManager } from '../utils';
 
@@ -127,7 +128,7 @@ export class OverlordDynamicConfigDialog extends React.Component<OverlordDynamic
     >
       <p>
         Edit the overlord dynamic configuration on the fly.
-        For more information please refer to the <a href="http://druid.io/docs/latest/configuration/index.html#overlord-dynamic-configuration" target="_blank">documentation</a>.
+        For more information please refer to the <ExternalLink href="http://druid.io/docs/latest/configuration/index.html#overlord-dynamic-configuration">documentation</ExternalLink>.
       </p>
       <AutoForm
         fields={[
diff --git a/web-console/src/dialogs/retention-dialog.test.ts b/web-console/src/dialogs/retention-dialog.spec.ts
similarity index 94%
rename from web-console/src/dialogs/retention-dialog.test.ts
rename to web-console/src/dialogs/retention-dialog.spec.ts
index 24c7ca8..ee37759 100644
--- a/web-console/src/dialogs/retention-dialog.test.ts
+++ b/web-console/src/dialogs/retention-dialog.spec.ts
@@ -17,7 +17,6 @@
  */
 
 import * as React from 'react';
-(React as any).PropTypes = require('prop-types'); // Trick blueprint 1.0.1 into accepting React 16 as React 15.
 
 import { reorderArray } from './retention-dialog';
 
diff --git a/web-console/src/entry.scss b/web-console/src/entry.scss
index 8505642..a414250 100644
--- a/web-console/src/entry.scss
+++ b/web-console/src/entry.scss
@@ -50,3 +50,24 @@ svg {
   height: 100%;
   width: 100%;
 }
+
+.label-info-text {
+  max-width: 400px;
+  padding: 15px;
+
+  p:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.bp3-form-group {
+  .bp3-form-content {
+    position: relative;
+
+    & > .bp3-popover-wrapper {
+      position: absolute;
+      right: 0;
+      top: 7px;
+    }
+  }
+}
diff --git a/web-console/src/components/auto-form.scss b/web-console/src/utils/druid-expression.ts
similarity index 72%
copy from web-console/src/components/auto-form.scss
copy to web-console/src/utils/druid-expression.ts
index 4b772c4..6737662 100644
--- a/web-console/src/components/auto-form.scss
+++ b/web-console/src/utils/druid-expression.ts
@@ -16,8 +16,15 @@
  * limitations under the License.
  */
 
-.auto-form {
-  .ace_scroller {
-    background-color: #212c36;
-  }
+
+const UNSAFE_CHAR = /[^a-z0-9 ,._\-;:(){}\[\]<>!@#$%^&*`~?]/ig;
+
+function escape(str: string): string {
+  return str.replace(UNSAFE_CHAR, (s) => {
+    return '\\u' + ('000' + s.charCodeAt(0).toString(16)).substr(-4);
+  });
+}
+
+export function escapeColumnName(name: string): string {
+  return `"${escape(name)}"`;
 }
diff --git a/web-console/src/utils/druid-query.tsx b/web-console/src/utils/druid-query.ts
similarity index 82%
rename from web-console/src/utils/druid-query.tsx
rename to web-console/src/utils/druid-query.ts
index 9dd4107..a9e8018 100644
--- a/web-console/src/utils/druid-query.tsx
+++ b/web-console/src/utils/druid-query.ts
@@ -19,14 +19,35 @@
 import axios from 'axios';
 import { AxiosResponse } from 'axios';
 
+export function parseHtmlError(htmlStr: string): string | null {
+  const startIndex = htmlStr.indexOf('</h3><pre>');
+  const endIndex = htmlStr.indexOf('\n\tat');
+  if (startIndex === -1 || endIndex === -1) return null;
+
+  return htmlStr
+    .substring(startIndex + 10, endIndex)
+    .replace(/&quot;/g, '"')
+    .replace(/&gt;/g, '>');
+}
+
 export function getDruidErrorMessage(e: any) {
   const data: any = ((e.response || {}).data || {});
-  return [
-    data.error,
-    data.errorMessage,
-    data.errorClass,
-    data.host ? `on host ${data.host}` : null
-  ].filter(Boolean).join(' / ') || e.message;
+  switch (typeof data) {
+    case 'object':
+      return [
+        data.error,
+        data.errorMessage,
+        data.errorClass,
+        data.host ? `on host ${data.host}` : null
+      ].filter(Boolean).join(' / ') || e.message;
+
+    case 'string':
+      const htmlResp = parseHtmlError(data);
+      return htmlResp ? `HTML Error: ${htmlResp}` : e.message;
+
+    default:
+      return e.message;
+  }
 }
 
 export async function queryDruidRune(runeQuery: Record<string, any>): Promise<any> {
diff --git a/web-console/src/utils/druid-time.ts b/web-console/src/utils/druid-time.ts
new file mode 100644
index 0000000..44b1761
--- /dev/null
+++ b/web-console/src/utils/druid-time.ts
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+export type DruidTimestampFormat = 'iso' | 'millis' | 'posix' | 'auto' | 'd/M/yyyy' | 'dd-M-yyyy hh:mm:ss a' |
+  'MM/dd/YYYY' | 'M/d/YY' | 'MM/dd/YYYY hh:mm:ss a' | 'YYYY-MM-dd HH:mm:ss' | 'YYYY-MM-dd HH:mm:ss.S';
+
+export const TIMESTAMP_FORMAT_VALUES: DruidTimestampFormat[] = [
+  'iso', 'millis', 'posix', 'MM/dd/YYYY hh:mm:ss a', 'MM/dd/YYYY', 'M/d/YY', 'd/M/yyyy',
+  'YYYY-MM-dd HH:mm:ss', 'YYYY-MM-dd HH:mm:ss.S'
+];
+
+const EXAMPLE_DATE_ISO = '2015-10-29T23:00:00.000Z';
+const EXAMPLE_DATE_VALUE = Date.parse(EXAMPLE_DATE_ISO);
+const MIN_MILLIS = 3.15576e11; // 3 years in millis, so Tue Jan 01 1980
+const MAX_MILLIS = EXAMPLE_DATE_VALUE * 10;
+const MIN_POSIX = MIN_MILLIS / 1000;
+const MAX_POSIX = MAX_MILLIS / 1000;
+
+// copied from http://goo.gl/0ejHHW with small tweak to make dddd not pass on its own
+// tslint:disable-next-line:max-line-length
+export const ISO_MATCHER = new RegExp(/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))(T((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)$/);
+export const JODA_TO_REGEXP_LOOKUP: Record<string, RegExp> = {
+  'd/M/yyyy': /^[12]?\d\/1?\d\/\d\d\d\d$/,
+  'MM/dd/YYYY': /^\d\d\/\d\d\/\d\d\d\d$/,
+  'M/d/YY': /^1?\d\/[12]?\d\/\d\d$/,
+  'd-M-yyyy hh:mm:ss a': /^[12]?\d-1?\d-\d\d\d\d \d\d:\d\d:\d\d [ap]m$/i,
+  'MM/dd/YYYY hh:mm:ss a' : /^\d\d\/\d\d\/\d\d\d\d \d\d:\d\d:\d\d [ap]m$/i,
+  'YYYY-MM-dd HH:mm:ss' : /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/,
+  'YYYY-MM-dd HH:mm:ss.S': /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d$/
+};
+
+export function timeFormatMatches(format: DruidTimestampFormat, value: string | number): boolean {
+  if (format === 'iso') {
+    return ISO_MATCHER.test(String(value));
+  }
+
+  if (format === 'millis') {
+    const absValue = Math.abs(Number(value));
+    return MIN_MILLIS < absValue && absValue < MAX_MILLIS;
+  }
+
+  if (format === 'posix') {
+    const absValue = Math.abs(Number(value));
+    return MIN_POSIX < absValue && absValue < MAX_POSIX;
+  }
+
+  const formatRegexp = JODA_TO_REGEXP_LOOKUP[format];
+  if (!formatRegexp) throw new Error(`unknown Druid format ${format}`);
+
+  return formatRegexp.test(String(value));
+}
+
+export function possibleDruidFormatForValues(values: any[]): DruidTimestampFormat | null {
+  return TIMESTAMP_FORMAT_VALUES.filter(format => {
+    return values.every(value => timeFormatMatches(format, value));
+  })[0] || null;
+}
diff --git a/web-console/src/utils/druid-type.ts b/web-console/src/utils/druid-type.ts
new file mode 100644
index 0000000..3073bb3
--- /dev/null
+++ b/web-console/src/utils/druid-type.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 { filterMap } from './general';
+import { DimensionMode, DimensionSpec, IngestionSpec, MetricSpec } from './ingestion-spec';
+import { deepDelete, deepSet } from './object-change';
+import { HeaderAndRows } from './sampler';
+
+export function guessTypeFromSample(sample: any[]): string {
+  const definedValues = sample.filter(v => v != null);
+  if (definedValues.length && definedValues.every(v => !isNaN(v) && (typeof v === 'number' || typeof v === 'string'))) {
+    if (definedValues.every(v => v % 1 === 0)) {
+      return 'long';
+    } else {
+      return 'float';
+    }
+  } else {
+    return 'string';
+  }
+}
+
+export function getColumnTypeFromHeaderAndRows(headerAndRows: HeaderAndRows, column: string): string {
+  return guessTypeFromSample(filterMap(headerAndRows.rows, (r: any) => r.parsed ? r.parsed[column] : null));
+}
+
+export function getDimensionSpecs(headerAndRows: HeaderAndRows, hasRollup: boolean): (string | DimensionSpec)[] {
+  return filterMap(headerAndRows.header, (h) => {
+    if (h === '__time') return null;
+    const guessedType = getColumnTypeFromHeaderAndRows(headerAndRows, h);
+    if (guessedType === 'string') return h;
+    if (hasRollup) return null;
+    return {
+      type: guessedType,
+      name: h
+    };
+  });
+}
+
+export function getMetricSecs(headerAndRows: HeaderAndRows): MetricSpec[] {
+  return [{ name: 'count', type: 'count' }].concat(filterMap(headerAndRows.header, (h) => {
+    if (h === '__time') return null;
+    const guessedType = getColumnTypeFromHeaderAndRows(headerAndRows, h);
+    switch (guessedType) {
+      case 'double': return { name: `sum_${h}`, type: 'doubleSum', fieldName: h };
+      case 'long': return { name: `sum_${h}`, type: 'longSum', fieldName: h };
+      default: return null;
+    }
+  }));
+}
+
+export function updateSchemaWithSample(spec: IngestionSpec, headerAndRows: HeaderAndRows, dimensionMode: DimensionMode, rollup: boolean): IngestionSpec {
+  let newSpec = spec;
+
+  if (dimensionMode === 'auto-detect') {
+    newSpec = deepSet(newSpec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensions', []);
+
+  } else {
+    newSpec = deepDelete(newSpec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensionExclusions');
+
+    const dimensions = getDimensionSpecs(headerAndRows, rollup);
+    if (dimensions) {
+      newSpec = deepSet(newSpec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensions', dimensions);
+    }
+
+  }
+
+  if (rollup) {
+    newSpec = deepSet(newSpec, 'dataSchema.granularitySpec.queryGranularity', 'HOUR');
+
+    const metrics = getMetricSecs(headerAndRows);
+    if (metrics) {
+      newSpec = deepSet(newSpec, 'dataSchema.metricsSpec', metrics);
+    }
+
+  } else {
+    newSpec = deepSet(newSpec, 'dataSchema.granularitySpec.queryGranularity', 'NONE');
+    newSpec = deepDelete(newSpec, 'dataSchema.metricsSpec');
+
+  }
+
+  newSpec = deepSet(newSpec, 'dataSchema.granularitySpec.rollup', rollup);
+  return newSpec;
+}
diff --git a/web-console/src/utils/example-ingestion-spec.ts b/web-console/src/utils/example-ingestion-spec.ts
new file mode 100644
index 0000000..0fd7614
--- /dev/null
+++ b/web-console/src/utils/example-ingestion-spec.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 { IngestionSpec } from './ingestion-spec';
+
+export function getWikipediaSpec(dataSourceSuffix: string): IngestionSpec {
+  return {
+    'type': 'index',
+    'dataSchema': {
+      'dataSource': 'wikipedia-' + dataSourceSuffix,
+      'parser': {
+        'type': 'string',
+        'parseSpec': {
+          'format': 'json',
+          'dimensionsSpec': {
+            'dimensions': [
+              'isRobot',
+              'channel',
+              'flags',
+              'isUnpatrolled',
+              'page',
+              'diffUrl',
+              {
+                'name': 'added',
+                'type': 'long'
+              },
+              'comment',
+              {
+                'name': 'commentLength',
+                'type': 'long'
+              },
+              'isNew',
+              'isMinor',
+              {
+                'name': 'delta',
+                'type': 'long'
+              },
+              'isAnonymous',
+              'user',
+              {
+                'name': 'deltaBucket',
+                'type': 'long'
+              },
+              {
+                'name': 'deleted',
+                'type': 'long'
+              },
+              'namespace'
+            ]
+          },
+          'timestampSpec': {
+            'column': 'timestamp',
+            'format': 'iso'
+          }
+        }
+      },
+      'granularitySpec': {
+        'type': 'uniform',
+        'segmentGranularity': 'DAY',
+        'rollup': false,
+        'queryGranularity': 'none'
+      },
+      'metricsSpec': []
+    },
+    'ioConfig': {
+      'type': 'index',
+      'firehose': {
+        'fetchTimeout': 300000,
+        'type': 'http',
+        'uris': [
+          'https://static.imply.io/data/wikipedia.json.gz'
+        ]
+      }
+    },
+    'tuningConfig': {
+      'type': 'index',
+      'forceExtendableShardSpecs': true,
+      'maxParseExceptions': 100,
+      'maxSavedParseExceptions': 10
+    }
+  };
+}
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 848f2c9..c69ef32 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -171,6 +171,14 @@ export function getHeadProp(results: Record<string, any>[], prop: string): any {
 
 // ----------------------------
 
+export function parseJson(json: string): any {
+  try {
+    return JSON.parse(json);
+  } catch (e) {
+    return undefined;
+  }
+}
+
 export function validJson(json: string): boolean {
   try {
     JSON.parse(json);
@@ -197,3 +205,14 @@ export function parseStringToJSON(s: string): JSON | null {
     return JSON.parse(s);
   }
 }
+
+export function filterMap<T, Q>(xs: T[], f: (x: T, i?: number) => Q | null | undefined): Q[] {
+  return (xs.map(f) as any).filter(Boolean);
+}
+
+export function sortWithPrefixSuffix(things: string[], prefix: string[], suffix: string[]): string[] {
+  const pre = things.filter((x) => prefix.includes(x)).sort();
+  const mid = things.filter((x) => !prefix.includes(x) && !suffix.includes(x)).sort();
+  const post = things.filter((x) => suffix.includes(x)).sort();
+  return pre.concat(mid, post);
+}
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 5ba83fa..0a4c9f9 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -19,6 +19,7 @@
 export * from './general';
 export * from './druid-query';
 export * from './query-manager';
+export * from './query-state';
 export * from './rune-decoder';
 export * from './table-column-selection-handler';
 export * from './local-storage-keys';
diff --git a/web-console/src/utils/ingestion-spec.tsx b/web-console/src/utils/ingestion-spec.tsx
new file mode 100644
index 0000000..f89dadb
--- /dev/null
+++ b/web-console/src/utils/ingestion-spec.tsx
@@ -0,0 +1,1143 @@
+/*
+ * 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 { Code } from '@blueprintjs/core';
+import { number } from 'prop-types';
+import * as React from 'react';
+
+import { Field } from '../components/auto-form';
+import { ExternalLink } from '../components/external-link';
+
+import { TIMESTAMP_FORMAT_VALUES } from './druid-time';
+import { deepGet, deepSet } from './object-change';
+
+export interface IngestionSpec {
+  type?: IngestionType;
+  dataSchema: DataSchema;
+  ioConfig: IoConfig;
+  tuningConfig?: TuningConfig;
+}
+
+export type IngestionType = 'kafka' | 'kinesis' | 'index_hadoop' | 'index' | 'index_parallel';
+
+// A combination of IngestionType and firehose
+export type IngestionComboType =
+  'kafka' |
+  'kinesis' |
+  'index:http' |
+  'index:local' |
+  'index:static-s3' |
+  'index:static-google-blobstore';
+
+function ingestionTypeToIoAndTuningConfigType(ingestionType: IngestionType): string {
+  switch (ingestionType) {
+    case 'kafka':
+    case 'kinesis':
+    case 'index':
+    case 'index_parallel':
+      return ingestionType;
+
+    case 'index_hadoop':
+      return 'hadoop';
+
+    default:
+      throw new Error(`unknown type '${ingestionType}'`);
+  }
+}
+
+export function getIngestionComboType(spec: IngestionSpec): IngestionComboType | null {
+  const ioConfig = deepGet(spec, 'ioConfig') || {};
+
+  switch (ioConfig.type) {
+    case 'kafka':
+    case 'kinesis':
+      return ioConfig.type;
+
+    case 'index':
+    case 'index_parallel':
+      const firehose = deepGet(spec, 'ioConfig.firehose') || {};
+      switch (firehose.type) {
+        case 'local':
+        case 'http':
+        case 'static-s3':
+        case 'static-google-blobstore':
+          return `index:${firehose.type}` as any;
+      }
+  }
+
+  return null;
+}
+
+// --------------
+
+export interface DataSchema {
+  dataSource: string;
+  parser: Parser;
+  transformSpec?: TransformSpec;
+  granularitySpec?: GranularitySpec;
+  metricsSpec?: MetricSpec[];
+}
+
+export interface Parser {
+  type?: string;
+  parseSpec: ParseSpec;
+}
+
+export interface ParseSpec {
+  format: string;
+  hasHeaderRow?: boolean;
+  skipHeaderRows?: number;
+  columns?: string[];
+  listDelimiter?: string;
+  pattern?: string;
+  'function'?: string;
+
+  timestampSpec: TimestampSpec;
+  dimensionsSpec: DimensionsSpec;
+  flattenSpec?: FlattenSpec;
+}
+
+export function hasParallelAbility(spec: IngestionSpec): boolean {
+  return spec.type === 'index' || spec.type === 'index_parallel';
+}
+
+export function isParallel(spec: IngestionSpec): boolean {
+  return spec.type === 'index_parallel';
+}
+
+export type DimensionMode = 'specific' | 'auto-detect';
+
+export function getDimensionMode(spec: IngestionSpec): DimensionMode {
+  const dimensions = deepGet(spec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensions') || [];
+  return Array.isArray(dimensions) && dimensions.length === 0 ? 'auto-detect' : 'specific';
+}
+
+export function getRollup(spec: IngestionSpec): boolean {
+  const specRollup = deepGet(spec, 'dataSchema.granularitySpec.rollup');
+  return typeof specRollup === 'boolean' ? specRollup : true;
+}
+
+export function changeParallel(spec: IngestionSpec, parallel: boolean): IngestionSpec {
+  if (!hasParallelAbility(spec)) return spec;
+  const newType = parallel ? 'index_parallel' : 'index';
+  let newSpec = spec;
+  newSpec = deepSet(newSpec, 'type', newType);
+  newSpec = deepSet(newSpec, 'ioConfig.type', newType);
+  newSpec = deepSet(newSpec, 'tuningConfig.type', newType);
+  return newSpec;
+}
+
+const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
+  {
+    name: 'format',
+    label: 'Parser to use',
+    type: 'string',
+    suggestions: ['json', 'csv', 'tsv', 'regex'],
+    info: <>
+      <p>The parser used to parse the data.</p>
+      <p>For more information see <ExternalLink href="http://druid.io/docs/latest/ingestion/data-formats.html">the documentation</ExternalLink>.</p>
+    </>
+  },
+  {
+    name: 'pattern',
+    type: 'string',
+    isDefined: (p: ParseSpec) => p.format === 'regex'
+  },
+  {
+    name: 'function',
+    type: 'string',
+    isDefined: (p: ParseSpec) => p.format === 'javascript'
+  },
+  {
+    name: 'hasHeaderRow',
+    type: 'boolean',
+    defaultValue: true,
+    isDefined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv'
+  },
+  {
+    name: 'skipHeaderRows',
+    type: 'number',
+    defaultValue: 0,
+    isDefined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
+    min: 0,
+    info: <>
+      If both skipHeaderRows and hasHeaderRow options are set, skipHeaderRows is first applied. For example, if you set skipHeaderRows to 2 and hasHeaderRow to true, Druid will skip the first two lines and then extract column information from the third line.
+    </>
+  },
+  {
+    name: 'columns',
+    type: 'string-array',
+    isDefined: (p: ParseSpec) => ((p.format === 'csv' || p.format === 'tsv') && !p.hasHeaderRow) || p.format === 'regex'
+  },
+  {
+    name: 'listDelimiter',
+    type: 'string',
+    defaultValue: '|',
+    isDefined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv'
+  }
+];
+
+export function getParseSpecFormFields() {
+  return PARSE_SPEC_FORM_FIELDS;
+}
+
+export function issueWithParser(parser: Parser | undefined): string | null {
+  if (!parser) return 'no parser';
+  if (parser.type === 'map') return null;
+
+  const { parseSpec } = parser;
+  if (!parseSpec) return 'no parse spec';
+  if (!parseSpec.format) return 'missing a format';
+  switch (parseSpec.format) {
+    case 'regex':
+      if (!parseSpec.pattern) return "must have a 'pattern'";
+      break;
+
+    case 'javascript':
+      if (!parseSpec['function']) return "must have a 'function'";
+      break;
+
+  }
+  return null;
+}
+
+export function parseSpecHasFlatten(parseSpec: ParseSpec): boolean {
+  return parseSpec.format === 'json';
+}
+
+export interface TimestampSpec {
+  column?: string;
+  format?: string;
+  missingValue?: string;
+}
+
+export function getTimestampSpecColumn(timestampSpec: TimestampSpec) {
+  // https://github.com/apache/incubator-druid/blob/master/core/src/main/java/org/apache/druid/data/input/impl/TimestampSpec.java#L44
+  return timestampSpec.column || 'timestamp';
+}
+
+const NO_SUCH_COLUMN = '!!!_no_such_column_!!!';
+
+const EMPTY_TIMESTAMP_SPEC: TimestampSpec = {
+  column: NO_SUCH_COLUMN,
+  missingValue: '2010-01-01T00:00:00Z'
+};
+
+export function getEmptyTimestampSpec() {
+  return EMPTY_TIMESTAMP_SPEC;
+}
+
+export function isColumnTimestampSpec(timestampSpec: TimestampSpec) {
+  return (deepGet(timestampSpec, 'column') || 'timestamp') !== NO_SUCH_COLUMN;
+}
+
+const TIMESTAMP_SPEC_FORM_FIELDS: Field<TimestampSpec>[] = [
+  {
+    name: 'column',
+    type: 'string',
+    isDefined: (timestampSpec: TimestampSpec) => isColumnTimestampSpec(timestampSpec)
+  },
+  {
+    name: 'format',
+    type: 'string',
+    suggestions: ['auto'].concat(TIMESTAMP_FORMAT_VALUES),
+    isDefined: (timestampSpec: TimestampSpec) => isColumnTimestampSpec(timestampSpec),
+    info: <p>
+      Please specify your timestamp format by using the suggestions menu or typing in a <ExternalLink href="https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">format string</ExternalLink>.
+    </p>
+  },
+  {
+    name: 'missingValue',
+    type: 'string',
+    isDefined: (timestampSpec: TimestampSpec) => !isColumnTimestampSpec(timestampSpec)
+  }
+];
+
+export function getTimestampSpecFormFields() {
+  return TIMESTAMP_SPEC_FORM_FIELDS;
+}
+
+export function issueWithTimestampSpec(timestampSpec: TimestampSpec | undefined): string | null {
+  if (!timestampSpec) return 'no spec';
+  if (!timestampSpec.column && !timestampSpec.missingValue) return 'timestamp spec is blank';
+  return null;
+}
+
+export interface DimensionsSpec {
+  dimensions?: (string | DimensionSpec)[];
+  dimensionExclusions?: string[];
+  spatialDimensions?: any[];
+}
+
+export interface DimensionSpec {
+  type: string;
+  name: string;
+  createBitmapIndex?: boolean;
+}
+
+const DIMENSION_SPEC_FORM_FIELDS: Field<DimensionSpec>[] = [
+  {
+    name: 'name',
+    type: 'string'
+  },
+  {
+    name: 'type',
+    type: 'string',
+    suggestions: ['string', 'long', 'float']
+  },
+  {
+    name: 'createBitmapIndex',
+    type: 'boolean',
+    defaultValue: true,
+    isDefined: (dimensionSpec: DimensionSpec) => dimensionSpec.type === 'string'
+  }
+];
+
+export function getDimensionSpecFormFields() {
+  return DIMENSION_SPEC_FORM_FIELDS;
+}
+
+export function getDimensionSpecName(dimensionSpec: string | DimensionSpec): string {
+  return typeof dimensionSpec === 'string' ? dimensionSpec : dimensionSpec.name;
+}
+
+export function getDimensionSpecType(dimensionSpec: string | DimensionSpec): string {
+  return typeof dimensionSpec === 'string' ? 'string' : dimensionSpec.type;
+}
+
+export function inflateDimensionSpec(dimensionSpec: string | DimensionSpec): DimensionSpec {
+  return typeof dimensionSpec === 'string' ? { name: dimensionSpec, type: 'string' } : dimensionSpec;
+}
+
+export interface FlattenSpec {
+  useFieldDiscovery?: boolean;
+  fields?: FlattenField[];
+}
+
+export interface FlattenField {
+  name: string;
+  type: string;
+  expr: string;
+}
+
+const FLATTEN_FIELD_FORM_FIELDS: Field<FlattenField>[] = [
+  {
+    name: 'name',
+    type: 'string',
+    placeholder: 'column_name'
+  },
+  {
+    name: 'type',
+    type: 'string',
+    suggestions: ['path', 'jq', 'root']
+  },
+  {
+    name: 'expr',
+    type: 'string',
+    placeholder: '$.thing',
+    isDefined: (flattenField: FlattenField) => flattenField.type === 'path' || flattenField.type === 'jq',
+    info: <>
+      Specify a flatten <ExternalLink href="http://druid.io/docs/latest/ingestion/flatten-json">expression</ExternalLink>.
+    </>
+  }
+];
+
+export function getFlattenFieldFormFields() {
+  return FLATTEN_FIELD_FORM_FIELDS;
+}
+
+export interface TransformSpec {
+  transforms: Transform[];
+  filter?: any;
+}
+
+export interface Transform {
+  type: string;
+  name: string;
+  expression: string;
+}
+
+const TRANSFORM_FORM_FIELDS: Field<Transform>[] = [
+  {
+    name: 'name',
+    type: 'string',
+    placeholder: 'output_name'
+  },
+  {
+    name: 'type',
+    type: 'string',
+    suggestions: ['expression']
+  },
+  {
+    name: 'expression',
+    type: 'string',
+    placeholder: '"foo" + "bar"',
+    info: <>
+      A valid Druid <ExternalLink href="http://druid.io/docs/latest/misc/math-expr.html">expression</ExternalLink>.
+    </>
+  }
+];
+
+export function getTransformFormFields() {
+  return TRANSFORM_FORM_FIELDS;
+}
+
+export interface GranularitySpec {
+  type?: string;
+  queryGranularity?: string;
+  segmentGranularity?: string;
+  rollup?: boolean;
+  intervals?: string;
+}
+
+export interface MetricSpec {
+  type: string;
+  name?: string;
+  fieldName?: string;
+  maxStringBytes?: number;
+  filterNullValues?: boolean;
+  fieldNames?: string[];
+  fnAggregate?: string;
+  fnCombine?: string;
+  fnReset?: string;
+  fields?: string[];
+  byRow?: boolean;
+  round?: boolean;
+  isInputHyperUnique?: boolean;
+  filter?: any;
+  aggregator?: MetricSpec;
+}
+
+const METRIC_SPEC_FORM_FIELDS: Field<MetricSpec>[] = [
+  {
+    name: 'name',
+    type: 'string'
+  },
+  {
+    name: 'type',
+    type: 'string',
+    suggestions: [
+      'count',
+      {
+        group: 'sum',
+        suggestions: [
+          'longSum',
+          'doubleSum',
+          'floatSum'
+        ]
+      },
+      {
+        group: 'min',
+        suggestions: [
+          'longMin',
+          'doubleMin',
+          'floatMin'
+        ]
+      },
+      {
+        group: 'max',
+        suggestions: [
+          'longMax',
+          'doubleMax',
+          'floatMax'
+        ]
+      },
+      {
+        group: 'first',
+        suggestions: [
+          'longFirst',
+          'doubleFirst',
+          'floatFirst'
+        ]
+      },
+      {
+        group: 'last',
+        suggestions: [
+          'longLast',
+          'doubleLast',
+          'floatLast'
+        ]
+      },
+      'cardinality',
+      'hyperUnique',
+      'filtered'
+    ]
+  },
+  {
+    name: 'fieldName',
+    type: 'string',
+    isDefined: m => {
+      return [
+        'longSum',
+        'doubleSum',
+        'floatSum',
+        'longMin',
+        'doubleMin',
+        'floatMin',
+        'longMax',
+        'doubleMax',
+        'floatMax',
+        'longFirst',
+        'doubleFirst',
+        'floatFirst',
+        'stringFirst',
+        'longLast',
+        'doubleLast',
+        'floatLast',
+        'stringLast',
+        'cardinality',
+        'hyperUnique'
+      ].includes(m.type);
+    }
+  },
+  {
+    name: 'maxStringBytes',
+    type: 'number',
+    defaultValue: 1024,
+    isDefined: m => {
+      return ['stringFirst', 'stringLast'].includes(m.type);
+    }
+  },
+  {
+    name: 'filterNullValues',
+    type: 'boolean',
+    defaultValue: false,
+    isDefined: m => {
+      return ['stringFirst', 'stringLast'].includes(m.type);
+    }
+  },
+  {
+    name: 'filter',
+    type: 'json',
+    isDefined: m => {
+      return m.type === 'filtered';
+    }
+  },
+  {
+    name: 'aggregator',
+    type: 'json',
+    isDefined: m => {
+      return m.type === 'filtered';
+    }
+  }
+  // ToDo: fill in approximates
+];
+
+export function getMetricSpecFormFields() {
+  return METRIC_SPEC_FORM_FIELDS;
+}
+
+export function getMetricSpecName(metricSpec: MetricSpec): string {
+  return metricSpec.name || (metricSpec.aggregator ? getMetricSpecName(metricSpec.aggregator) : '?');
+}
+
+// --------------
+
+export interface IoConfig {
+  type: string;
+  firehose?: Firehose;
+  appendToExisting?: boolean;
+  topic?: string;
+  consumerProperties?: any;
+  replicas?: number;
+  taskCount?: number;
+  taskDuration?: string;
+  startDelay?: string;
+  period?: string;
+  useEarliestOffset?: boolean;
+  stream?: string;
+  region?: string;
+  useEarliestSequenceNumber?: boolean;
+}
+
+export interface Firehose {
+  type: string;
+  baseDir?: string;
+  filter?: string;
+  uris?: string[];
+  prefixes?: string[];
+  blobs?: { bucket: string, path: string }[];
+  fetchTimeout?: number;
+}
+
+export function getIoConfigFormFields(ingestionComboType: IngestionComboType): Field<IoConfig>[] {
+  const firehoseType: Field<IoConfig> = {
+    name: 'firehose.type',
+    label: 'Firehose type',
+    type: 'string',
+    suggestions: ['local', 'http', 'static-s3', 'static-google-blobstore'],
+    info: <>
+      <p>
+        Druid connects to raw data through <ExternalLink href="http://druid.io/docs/latest/ingestion/firehose.html">firehoses</ExternalLink>.
+        You can change your selected firehose here.
+      </p>
+    </>
+  };
+
+  switch (ingestionComboType) {
+    case 'index:http':
+      return [
+        firehoseType,
+        {
+          name: 'firehose.uris',
+          label: 'URIs',
+          type: 'string-array',
+          placeholder: 'https://example.com/path/to/file.ext',
+          info: <>
+            <p>The full URI of your file. To ingest from multiple URIs, use commas to separate each individual URI.</p>
+          </>
+        }
+      ];
+
+    case 'index:local':
+      return [
+        firehoseType,
+        {
+          name: 'firehose.baseDir',
+          label: 'Base directory',
+          type: 'string',
+          placeholder: '/path/to/files/',
+          info: <>
+            <ExternalLink href="http://druid.io/docs/latest/ingestion/firehose.html#localfirehose">firehose.baseDir</ExternalLink>
+            <p>Specifies the directory to search recursively for files to be ingested.</p>
+          </>
+        },
+        {
+          name: 'firehose.filter',
+          label: 'File filter',
+          type: 'string',
+          defaultValue: '*.*',
+          info: <>
+            <ExternalLink href="http://druid.io/docs/latest/ingestion/firehose.html#localfirehose">firehose.filter</ExternalLink>
+            <p>A wildcard filter for files. See <ExternalLink href="https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/filefilter/WildcardFileFilter.html">here</ExternalLink> for format information.</p>
+          </>
+        }
+      ];
+
+    case 'index:static-s3':
+      return [
+        firehoseType,
+        {
+          name: 'firehose.uris',
+          label: 'S3 URIs',
+          type: 'string-array',
+          placeholder: 's3://your-bucket/some-file.extension',
+          isDefined: (ioConfig) => !deepGet(ioConfig, 'firehose.prefixes'),
+          info: <>
+            <p>The full S3 URI of your file. To ingest from multiple URIs, use commas to separate each individual URI.</p>
+            <p>Either S3 URIs or S3 prefixes must be set.</p>
+          </>
+        },
+        {
+          name: 'firehose.prefixes',
+          label: 'S3 prefixes',
+          type: 'string-array',
+          placeholder: 's3://your-bucket/some-path',
+          isDefined: (ioConfig) => !deepGet(ioConfig, 'firehose.uris'),
+          info: <>
+            <p>A list of paths (with bucket) where your files are stored.</p>
+            <p>Either S3 URIs or S3 prefixes must be set.</p>
+          </>
+        }
+      ];
+
+    case 'index:static-google-blobstore':
+      return [
+        firehoseType,
+        {
+          name: 'firehose.blobs',
+          label: 'Google blobs',
+          type: 'json',
+          info: <>
+            <p>JSON array of <ExternalLink href="http://druid.io/docs/latest/development/extensions-contrib/google.html">Google Blobs</ExternalLink>.</p>
+          </>
+        }
+      ];
+
+    case 'kafka':
+      return [
+        {
+          name: 'consumerProperties.{bootstrap.servers}',
+          label: 'Bootstrap servers',
+          type: 'string',
+          info: <>
+            <ExternalLink href="http://druid.io/docs/latest/development/extensions-core/kafka-ingestion#kafkasupervisorioconfig">consumerProperties</ExternalLink>
+            <p>A list of Kafka brokers in the form: <Code>{`<BROKER_1>:<PORT_1>,<BROKER_2>:<PORT_2>,...`}</Code></p>
+          </>
+        },
+        {
+          name: 'topic',
+          type: 'string',
+          isDefined: (i: IoConfig) => i.type === 'kafka'
+        },
+        {
+          name: 'consumerProperties',
+          type: 'json',
+          defaultValue: {},
+          info: <>
+            <ExternalLink href="http://druid.io/docs/latest/development/extensions-core/kafka-ingestion#kafkasupervisorioconfig">consumerProperties</ExternalLink>
+            <p>A map of properties to be passed to the Kafka consumer.</p>
+          </>
+        }
+      ];
+
+    case 'kinesis':
+      return [
+        {
+          name: 'stream',
+          type: 'string'
+        },
+        {
+          name: 'region',
+          type: 'string'
+        },
+        {
+          name: 'useEarliestOffset',
+          type: 'boolean',
+          defaultValue: true,
+          isDefined: (i: IoConfig) => i.type === 'kafka' || i.type === 'kinesis'
+        },
+        {
+          name: 'useEarliestSequenceNumber',
+          type: 'boolean',
+          isDefined: (i: IoConfig) => i.type === 'kinesis'
+        }
+      ];
+  }
+
+  throw new Error(`unknown input type ${ingestionComboType}`);
+}
+
+function nonEmptyArray(a: any) {
+  return Array.isArray(a) && Boolean(a.length);
+}
+
+function issueWithFirehose(firehose: Firehose | undefined): string | null {
+  if (!firehose) return 'does not exist';
+  if (!firehose.type) return 'missing a type';
+  switch (firehose.type) {
+    case 'local':
+      if (!firehose.baseDir) return "must have a 'baseDir'";
+      if (!firehose.filter) return "must have a 'filter'";
+      break;
+
+    case 'http':
+      if (!nonEmptyArray(firehose.uris)) return 'must have at least one uri';
+      break;
+
+    case 'static-s3':
+      if (!nonEmptyArray(firehose.uris) && !nonEmptyArray(firehose.prefixes)) return 'must have at least one uri or prefix';
+      break;
+
+    case 'static-google-blobstore':
+      if (!nonEmptyArray(firehose.blobs)) return 'must have at least one blob';
+      break;
+  }
+  return null;
+}
+
+export function issueWithIoConfig(ioConfig: IoConfig | undefined): string | null {
+  if (!ioConfig) return 'does not exist';
+  if (!ioConfig.type) return 'missing a type';
+  switch (ioConfig.type) {
+    case 'index':
+    case 'index_parallel':
+      if (issueWithFirehose(ioConfig.firehose)) return `firehose: '${issueWithFirehose(ioConfig.firehose)}'`;
+      break;
+
+    case 'kafka':
+      if (!ioConfig.topic) return 'must have a topic';
+      break;
+
+    case 'kinesis':
+      // if (!ioConfig.stream) return "must have a stream";
+      break;
+  }
+
+  return null;
+}
+
+export function getIoConfigTuningFormFields(ingestionComboType: IngestionComboType): Field<IoConfig>[] {
+  switch (ingestionComboType) {
+    case 'index:http':
+    case 'index:static-s3':
+    case 'index:static-google-blobstore':
+      const objectType = ingestionComboType === 'index:http' ? 'http' : 'S3';
+      return [
+        {
+          name: 'firehose.maxCacheCapacityBytes',
+          label: 'Max cache capacity bytes',
+          type: 'number',
+          defaultValue: 1073741824,
+          info: <>
+            <p>Maximum size of the cache space in bytes. 0 means disabling cache. Cached files are not removed until the ingestion task completes.</p>
+          </>
+        },
+        {
+          name: 'firehose.maxFetchCapacityBytes',
+          label: 'Max fetch capacity bytes',
+          type: 'number',
+          defaultValue: 1073741824,
+          info: <>
+            <p>Maximum size of the fetch space in bytes. 0 means disabling prefetch. Prefetched files are removed immediately once they are read.</p>
+          </>
+        },
+        {
+          name: 'firehose.prefetchTriggerBytes',
+          label: 'Prefetch trigger bytes',
+          type: 'number',
+          info: <>
+            <p>Threshold to trigger prefetching {objectType} objects.</p>
+            <p>Default: maxFetchCapacityBytes / 2</p>
+          </>
+        },
+        {
+          name: 'firehose.fetchTimeout',
+          label: 'Fetch timeout',
+          type: 'number',
+          defaultValue: 60000,
+          info: <>
+            <p>Timeout for fetching a http object.</p>
+          </>
+        },
+        {
+          name: 'firehose.maxFetchRetry',
+          label: 'Max fetch retry',
+          type: 'number',
+          defaultValue: 3,
+          info: <>
+            <p>Maximum retry for fetching a {objectType} object.</p>
+          </>
+        }
+      ];
+
+    case 'index:local':
+      return [];
+
+    case 'kafka':
+      return [
+        // ToDo: fill this in
+      ];
+
+    case 'kinesis':
+      return [
+        // ToDo: fill this in
+      ];
+  }
+
+  throw new Error(`unknown input type ${ingestionComboType}`);
+}
+
+// ---------------------------------------
+
+function filenameFromPath(path: string | undefined): string | null {
+  if (!path) return null;
+  const m = path.match(/([^\/.]+)[^\/]*?\/?$/);
+  return m ? m[1] : null;
+}
+
+export function fillDataSourceName(spec: IngestionSpec): IngestionSpec {
+  const ioConfig = deepGet(spec, 'ioConfig');
+  if (!ioConfig) return spec;
+  const possibleName = guessDataSourceName(ioConfig);
+  if (!possibleName) return spec;
+  return deepSet(spec, 'dataSchema.dataSource', possibleName);
+}
+
+export function guessDataSourceName(ioConfig: IoConfig): string | null {
+  switch (ioConfig.type) {
+    case 'index':
+    case 'index_parallel':
+      const firehose = ioConfig.firehose;
+      if (!firehose) return null;
+
+      switch (firehose.type) {
+        case 'local':
+          return filenameFromPath(firehose.baseDir);
+
+        case 'static-s3':
+          return filenameFromPath((firehose.uris || [])[0] || (firehose.prefixes || [])[0]);
+
+        case 'http':
+          return filenameFromPath(firehose.uris ? firehose.uris[0] : undefined);
+      }
+
+      return null;
+
+    case 'kafka':
+      return ioConfig.topic || null;
+
+    case 'kinesis':
+      return ioConfig.stream || null;
+
+    default:
+      return null;
+  }
+}
+
+// --------------
+
+export interface TuningConfig {
+  type: string;
+  targetPartitionSize?: number;
+  maxRowsInMemory?: number;
+  maxBytesInMemory?: number;
+  maxTotalRows?: number;
+  numShards?: number;
+  maxPendingPersists?: number;
+  indexSpec?: IndexSpec;
+  forceExtendableShardSpecs?: boolean;
+  forceGuaranteedRollup?: boolean;
+  reportParseExceptions?: boolean;
+  pushTimeout?: number;
+  segmentWriteOutMediumFactory?: any;
+  // ...
+  maxParseExceptions?: number;
+  maxSavedParseExceptions?: number;
+}
+
+const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
+  {
+    name: 'maxRowsInMemory',
+    type: 'number',
+    defaultValue: 1000000,
+    info: <>
+      Used in determining when intermediate persists to disk should occur.
+    </>
+  },
+  {
+    name: 'maxBytesInMemory',
+    type: 'number',
+    placeholder: 'Default: 1/6 of max JVM memory',
+    info: <>
+      Used in determining when intermediate persists to disk should occur.
+    </>
+  },
+  {
+    name: 'maxPendingPersists',
+    type: 'number'
+  },
+  {
+    name: 'forceExtendableShardSpecs',
+    type: 'boolean'
+  },
+  {
+    name: 'reportParseExceptions',
+    type: 'boolean'
+  },
+  {
+    name: 'pushTimeout',
+    type: 'number',
+    defaultValue: 0,
+    info: <>
+      Milliseconds to wait for pushing segments.
+      It must be >= 0, where 0 means to wait forever.
+    </>
+  },
+  {
+    name: 'maxNumSubTasks',
+    type: 'number',
+    defaultValue: 1,
+    info: <>
+      Maximum number of tasks which can be run at the same time.
+      The supervisor task would spawn worker tasks up to maxNumSubTasks regardless of the available task slots.
+      If this value is set to 1, the supervisor task processes data ingestion on its own instead of spawning worker tasks.
+      If this value is set to too large, too many worker tasks can be created which might block other ingestion.
+    </>
+  },
+  {
+    name: 'maxRetry',
+    type: 'number',
+    defaultValue: 3,
+    info: <>
+      Maximum number of retries on task failures.
+    </>
+  },
+  {
+    name: 'taskStatusCheckPeriodMs',
+    type: 'number',
+    defaultValue: 1000,
+    info: <>
+      Polling period in milliseconds to check running task statuses.
+    </>
+  },
+  {
+    name: 'chatHandlerTimeout',
+    type: 'string',
+    defaultValue: 'PT10S',
+    info: <>
+      Timeout for reporting the pushed segments in worker tasks.
+    </>
+  },
+  {
+    name: 'chatHandlerNumRetries',
+    type: 'number',
+    defaultValue: 5,
+    info: <>
+      Retries for reporting the pushed segments in worker tasks.
+    </>
+  }
+];
+
+export function getTuningSpecFormFields() {
+  return TUNING_CONFIG_FORM_FIELDS;
+}
+
+export interface IndexSpec {
+  bitmap?: Bitmap;
+  dimensionCompression?: string;
+  metricCompression?: string;
+  longEncoding?: string;
+}
+
+export interface Bitmap {
+  type: string;
+  compressRunOnSerialization?: boolean;
+}
+
+// --------------
+
+export function getBlankSpec(ingestionType: IngestionType = 'index', firehoseType: string | null = null): IngestionSpec {
+  const ioAndTuningConfigType = ingestionTypeToIoAndTuningConfigType(ingestionType);
+
+  const granularitySpec: GranularitySpec = {
+    type: 'uniform',
+    segmentGranularity: ['index', 'index_parallel'].includes(ingestionType) ? 'DAY' : 'HOUR',
+    queryGranularity: 'HOUR'
+  };
+
+  const spec: IngestionSpec = {
+    type: ingestionType,
+    dataSchema: {
+      dataSource: 'new-data-source',
+      granularitySpec
+    },
+    ioConfig: {
+      type: ioAndTuningConfigType
+    },
+    tuningConfig: {
+      type: ioAndTuningConfigType
+    }
+  } as any;
+
+  if (firehoseType) {
+    spec.ioConfig.firehose = {
+      type: firehoseType
+    };
+  }
+
+  return spec;
+}
+
+export function fillParser(spec: IngestionSpec, sampleData: string[]): IngestionSpec {
+  if (deepGet(spec, 'ioConfig.firehose.type') === 'sql') {
+    return deepSet(spec, 'dataSchema.parser', { type: 'map' });
+  }
+
+  const parseSpec =  guessParseSpec(sampleData);
+  if (!parseSpec) return spec;
+
+  return deepSet(spec, 'dataSchema.parser', { type: 'string', parseSpec });
+}
+
+function guessParseSpec(sampleData: string[]): ParseSpec | null {
+  const sampleDatum = sampleData[0];
+  if (!sampleDatum) return null;
+
+  if (sampleDatum.startsWith('{') && sampleDatum.endsWith('}')) {
+    return parseSpecFromFormat('json');
+  }
+
+  if (sampleDatum.split('\t').length > 3) {
+    return parseSpecFromFormat('tsv', !/\t\d+\t/.test(sampleDatum));
+  }
+
+  if (sampleDatum.split(',').length > 3) {
+    return parseSpecFromFormat('csv', !/,\d+,/.test(sampleDatum));
+  }
+
+  return parseSpecFromFormat('regex');
+}
+
+function parseSpecFromFormat(format: string, hasHeaderRow: boolean | null = null): ParseSpec {
+  const parseSpec: ParseSpec = {
+    format,
+    timestampSpec: {},
+    dimensionsSpec: {}
+  };
+
+  if (typeof hasHeaderRow === 'boolean') {
+    parseSpec.hasHeaderRow = hasHeaderRow;
+  }
+
+  return parseSpec;
+}
+
+export type DruidFilter = Record<string, any>;
+
+export interface DimensionFiltersWithRest {
+  dimensionFilters: DruidFilter[];
+  restFilter: DruidFilter | null;
+}
+
+export function splitFilter(filter: DruidFilter | null): DimensionFiltersWithRest {
+  const inputAndFilters: DruidFilter[] = filter ? ((filter.type === 'and' && Array.isArray(filter.fields)) ? filter.fields : [filter]) : [];
+  const dimensionFilters: DruidFilter[] = inputAndFilters.filter(f => typeof f.dimension === 'string');
+  const restFilters: DruidFilter[] = inputAndFilters.filter(f => typeof f.dimension !== 'string');
+
+  return {
+    dimensionFilters,
+    restFilter: restFilters.length ? (restFilters.length > 1 ? { type: 'and', filters: restFilters } : restFilters[0]) : null
+  };
+}
+
+export function joinFilter(dimensionFiltersWithRest: DimensionFiltersWithRest): DruidFilter | null {
+  const { dimensionFilters, restFilter } = dimensionFiltersWithRest;
+  let newFields = dimensionFilters || [];
+  if (restFilter && restFilter.type) newFields = newFields.concat([restFilter]);
+
+  if (!newFields.length) return null;
+  if (newFields.length === 1) return newFields[0];
+  return { type: 'and', fields: newFields };
+}
+
+const FILTER_FORM_FIELDS: Field<DruidFilter>[] = [
+  {
+    name: 'type',
+    type: 'string',
+    suggestions: ['selector', 'in']
+  },
+  {
+    name: 'dimension',
+    type: 'string'
+  },
+  {
+    name: 'value',
+    type: 'string',
+    isDefined: (druidFilter: DruidFilter) => druidFilter.type === 'selector'
+  },
+  {
+    name: 'values',
+    type: 'string-array',
+    isDefined: (druidFilter: DruidFilter) => druidFilter.type === 'in'
+  }
+];
+
+export function getFilterFormFields() {
+  return FILTER_FORM_FIELDS;
+}
diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx
index f79cc08..0c88158 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -17,6 +17,7 @@
  */
 
 export const LocalStorageKeys = {
+  INGESTION_SPEC: 'ingestion-spec' as 'ingestion-spec',
   DATASOURCE_TABLE_COLUMN_SELECTION: 'datasource-table-column-selection' as 'datasource-table-column-selection',
   SEGMENT_TABLE_COLUMN_SELECTION: 'segment-table-column-selection' as 'segment-table-column-selection',
   SUPERVISOR_TABLE_COLUMN_SELECTION: 'supervisor-table-column-selection' as 'supervisor-table-column-selection',
diff --git a/web-console/src/utils/object-change.spec.ts b/web-console/src/utils/object-change.spec.ts
new file mode 100644
index 0000000..5b83472
--- /dev/null
+++ b/web-console/src/utils/object-change.spec.ts
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { deepDelete, deepGet, deepSet, makePath, parsePath } from './object-change';
+
+describe('object-change', () => {
+  describe('parsePath', () => {
+    it('works', () => {
+      expect(parsePath('hello.wow.0')).toEqual(['hello', 'wow', '0']);
+      expect(parsePath('hello.{wow.moon}.0')).toEqual(['hello', 'wow.moon', '0']);
+      expect(parsePath('hello.#.0.[append]')).toEqual(['hello', '#', '0', '[append]']);
+    });
+
+  });
+
+  describe('makePath', () => {
+    it('works', () => {
+      expect(makePath(['hello', 'wow', '0'])).toEqual('hello.wow.0');
+      expect(makePath(['hello', 'wow.moon', '0'])).toEqual('hello.{wow.moon}.0');
+    });
+
+  });
+
+  describe('deepGet', () => {
+    const thing = {
+      hello: {
+        'consumer.props': 'lol',
+        wow: [
+          'a',
+          { test: 'moon' }
+        ]
+      },
+      zetrix: null
+    };
+
+    it('works', () => {
+      expect(deepGet(thing, 'hello.wow.0')).toEqual('a');
+      expect(deepGet(thing, 'hello.wow.4')).toEqual(undefined);
+      expect(deepGet(thing, 'hello.{consumer.props}')).toEqual('lol');
+    });
+
+  });
+
+  describe('deepSet', () => {
+    const thing = {
+      hello: {
+        wow: [
+          'a',
+          { test: 'moon' }
+        ]
+      },
+      zetrix: null
+    };
+
+    it('works to set an existing thing', () => {
+      expect(deepSet(thing, 'hello.wow.0', 5)).toEqual({
+        hello: {
+          wow: [
+            5,
+            {
+              test: 'moon'
+            }
+          ]
+        },
+        zetrix: null
+      });
+    });
+
+    it('works to set a non-existing thing', () => {
+      expect(deepSet(thing, 'lets.do.this.now', 5)).toEqual({
+        hello: {
+          wow: [
+            'a',
+            {
+              test: 'moon'
+            }
+          ]
+        },
+        lets: {
+          do: {
+            this: {
+              now: 5
+            }
+          }
+        },
+        zetrix: null
+      });
+    });
+
+    it('works to set an existing array', () => {
+      expect(deepSet(thing, 'hello.wow.[append]', 5)).toEqual({
+        hello: {
+          wow: [
+            'a',
+            {
+              test: 'moon'
+            },
+            5
+          ]
+        },
+        zetrix: null
+      });
+    });
+
+  });
+
+  describe('deepDelete', () => {
+    const thing = {
+      hello: {
+        moon: 1,
+        wow: [
+          'a',
+          { test: 'moon' }
+        ]
+      },
+      zetrix: null
+    };
+
+    it('works to delete an existing thing', () => {
+      expect(deepDelete(thing, 'hello.wow')).toEqual({
+        hello: { moon: 1 },
+        zetrix: null
+      });
+    });
+
+    it('works is harmless to delete a non-existing thing', () => {
+      expect(deepDelete(thing, 'hello.there.lol.why')).toEqual(thing);
+    });
+
+    it('removes things completely', () => {
+      expect(deepDelete(deepDelete(thing, 'hello.wow'), 'hello.moon')).toEqual({
+        zetrix: null
+      });
+    });
+
+    it('works with arrays', () => {
+      expect(JSON.parse(JSON.stringify(deepDelete(thing, 'hello.wow.0')))).toEqual({
+        hello: {
+          moon: 1,
+          wow: [
+            {
+              test: 'moon'
+            }
+          ]
+        },
+        zetrix: null
+      });
+    });
+
+  });
+
+});
diff --git a/web-console/src/utils/object-change.ts b/web-console/src/utils/object-change.ts
new file mode 100644
index 0000000..39094ec
--- /dev/null
+++ b/web-console/src/utils/object-change.ts
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+export function shallowCopy(v: any): any {
+  return Array.isArray(v) ? v.slice() : Object.assign({}, v);
+}
+
+function isEmpty(v: any): boolean {
+  return !(Array.isArray(v) ? v.length : Object.keys(v).length);
+}
+
+export function parsePath(path: string): string[] {
+  const parts: string[] = [];
+  let rest = path;
+  while (rest) {
+    const escapedMatch = rest.match(/^\{([^{}]*)\}(?:\.(.*))?$/);
+    if (escapedMatch) {
+      parts.push(escapedMatch[1]);
+      rest = escapedMatch[2];
+      continue;
+    }
+
+    const normalMatch = rest.match(/^([^.]*)(?:\.(.*))?$/);
+    if (normalMatch) {
+      parts.push(normalMatch[1]);
+      rest = normalMatch[2];
+      continue;
+    }
+
+    throw new Error(`Could not parse path ${path}`);
+  }
+
+  return parts;
+}
+
+export function makePath(parts: string[]): string {
+  return parts.map(p => p.includes('.') ? `{${p}}` : p).join('.');
+}
+
+function isAppend(key: string): boolean {
+  return key === '[append]' || key === '-1';
+}
+
+export function deepGet<T extends Record<string, any>>(value: T, path: string): any {
+  const parts = parsePath(path);
+  for (const part of parts) {
+    value = (value || {})[part];
+  }
+  return value;
+}
+
+export function deepSet<T extends Record<string, any>>(value: T, path: string, x: any): T {
+  const parts = parsePath(path);
+  let myKey = parts.shift() as string; // Must be defined
+  const valueCopy = shallowCopy(value);
+  if (Array.isArray(valueCopy) && isAppend(myKey)) myKey = String(valueCopy.length);
+  if (parts.length) {
+    const nextKey = parts[0];
+    const rest = makePath(parts);
+    valueCopy[myKey] = deepSet(value[myKey] || (isAppend(nextKey) ? [] : {}), rest, x);
+  } else {
+    valueCopy[myKey] = x;
+  }
+  return valueCopy;
+}
+
+export function deepDelete<T extends Record<string, any>>(value: T, path: string): T {
+  const valueCopy = shallowCopy(value);
+  const parts = parsePath(path);
+  const firstKey = parts.shift() as string; // Must be defined
+  if (parts.length) {
+    const firstKeyValue = value[firstKey];
+    if (firstKeyValue) {
+      const restPath = makePath(parts);
+      const prunedFirstKeyValue = deepDelete(value[firstKey], restPath);
+
+      if (isEmpty(prunedFirstKeyValue)) {
+        delete valueCopy[firstKey];
+      } else {
+        valueCopy[firstKey] = prunedFirstKeyValue;
+      }
+    } else {
+      delete valueCopy[firstKey];
+    }
+
+  } else {
+    if (Array.isArray(valueCopy) && !isNaN(Number(firstKey))) {
+      valueCopy.splice(Number(firstKey), 1);
+    } else {
+      delete valueCopy[firstKey];
+    }
+  }
+  return valueCopy;
+}
+
+export function whitelistKeys(obj: Record<string, any>, whitelist: string[]): Record<string, any> {
+  const newObj: Record<string, any> = {};
+  for (const w of whitelist) {
+    if (Object.prototype.hasOwnProperty.call(obj, w)) {
+      newObj[w] = obj[w];
+    }
+  }
+  return newObj;
+}
diff --git a/web-console/src/utils/query-manager.tsx b/web-console/src/utils/query-manager.tsx
index 22fc3f7..7dd347d 100644
--- a/web-console/src/utils/query-manager.tsx
+++ b/web-console/src/utils/query-manager.tsx
@@ -18,7 +18,7 @@
 
 import debounce = require('lodash.debounce');
 
-export interface QueryState<R> {
+export interface QueryStateInt<R> {
   result: R | null;
   loading: boolean;
   error: string | null;
@@ -26,20 +26,20 @@ export interface QueryState<R> {
 
 export interface QueryManagerOptions<Q, R> {
   processQuery: (query: Q) => Promise<R>;
-  onStateChange?: (queryResolve: QueryState<R>) => void;
+  onStateChange?: (queryResolve: QueryStateInt<R>) => void;
   debounceIdle?: number;
   debounceLoading?: number;
 }
 
 export class QueryManager<Q, R> {
   private processQuery: (query: Q) => Promise<R>;
-  private onStateChange?: (queryResolve: QueryState<R>) => void;
+  private onStateChange?: (queryResolve: QueryStateInt<R>) => void;
 
   private terminated = false;
   private nextQuery: Q;
   private lastQuery: Q;
   private actuallyLoading = false;
-  private state: QueryState<R> = {
+  private state: QueryStateInt<R> = {
     result: null,
     loading: false,
     error: null
@@ -64,7 +64,7 @@ export class QueryManager<Q, R> {
     }
   }
 
-  private setState(queryState: QueryState<R>) {
+  private setState(queryState: QueryStateInt<R>) {
     this.state = queryState;
     if (this.onStateChange && !this.terminated) {
       this.onStateChange(queryState);
@@ -130,7 +130,7 @@ export class QueryManager<Q, R> {
     return this.lastQuery;
   }
 
-  public getState(): QueryState<R> {
+  public getState(): QueryStateInt<R> {
     return this.state;
   }
 
diff --git a/web-console/src/components/header-bar.scss b/web-console/src/utils/query-state.ts
similarity index 50%
copy from web-console/src/components/header-bar.scss
copy to web-console/src/utils/query-state.ts
index a35d9f1..5f5f6e3 100644
--- a/web-console/src/components/header-bar.scss
+++ b/web-console/src/utils/query-state.ts
@@ -16,29 +16,38 @@
  * limitations under the License.
  */
 
-.header-bar {
-  overflow: hidden;
+export type QueryStateState = 'init' | 'loading' | 'data' | 'error';
 
-  .logo {
-    position: relative;
-    width: 100px;
-    height: 50px;
+export class QueryState<T> {
+  static INIT: QueryState<any> = new QueryState({});
 
-    svg {
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      transform: translate(-50%,-50%);
-      height: 75px;
+  public state: QueryStateState = 'init';
+  public error?: string | null;
+  public data?: T | null;
+
+  constructor(opts: { loading?: boolean, error?: string, data?: T }) {
+    if (opts.error) {
+      if (opts.data) {
+        throw new Error('can not have both error and data');
+      } else {
+        this.state = 'error';
+        this.error = opts.error;
+      }
+    } else {
+      if (opts.data) {
+        this.state = 'data';
+        this.data = opts.data;
+      } else {
+        this.state = opts.loading ? 'loading' : 'init';
+      }
     }
   }
 
-  .config-popover .bp3-popover-content,
-  .legacy-popover .bp3-popover-content {
-    width: 240px;
+  isInit(): boolean {
+    return this.state === 'init';
   }
 
-  .help-popover .bp3-popover-content {
-    width: 180px;
+  isLoading(): boolean {
+    return this.state === 'loading';
   }
 }
diff --git a/web-console/src/utils/sampler.ts b/web-console/src/utils/sampler.ts
new file mode 100644
index 0000000..85b1844
--- /dev/null
+++ b/web-console/src/utils/sampler.ts
@@ -0,0 +1,342 @@
+/*
+ * 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 axios from 'axios';
+
+import { getDruidErrorMessage } from './druid-query';
+import { filterMap, sortWithPrefixSuffix } from './general';
+import {
+  DimensionsSpec,
+  getEmptyTimestampSpec,
+  IngestionSpec,
+  IoConfig, MetricSpec,
+  Parser,
+  ParseSpec,
+  Transform, TransformSpec
+} from './ingestion-spec';
+import { deepGet, deepSet, shallowCopy, whitelistKeys } from './object-change';
+import { QueryState } from './query-state';
+
+const SAMPLER_URL = `/druid/indexer/v1/sampler`;
+const BASE_SAMPLER_CONFIG: SamplerConfig = {
+  // skipCache: true,
+  numRows: 500
+};
+
+export interface SampleSpec {
+  type: 'index';
+  spec: IngestionSpec;
+  samplerConfig: SamplerConfig;
+}
+
+export interface SamplerConfig {
+  numRows?: number;
+  cacheKey?: string;
+  skipCache?: boolean;
+}
+
+export interface SampleResponse {
+  cacheKey?: string;
+  data: SampleEntry[];
+}
+
+export interface SampleEntry {
+  raw: string;
+  parsed?: Record<string, any>;
+  unparseable?: boolean;
+  error?: string;
+}
+
+export interface HeaderAndRows {
+  header: string[];
+  rows: SampleEntry[];
+}
+
+function dedupe(xs: string[]): string[] {
+  const seen: Record<string, boolean> = {};
+  return xs.filter((x) => {
+    if (seen[x]) {
+      return false;
+    } else {
+      seen[x] = true;
+      return true;
+    }
+  });
+}
+
+export function headerFromSampleResponse(sampleResponse: SampleResponse, ignoreColumn?: string): string[] {
+  let columns = sortWithPrefixSuffix(dedupe(
+    [].concat(...(filterMap(sampleResponse.data, s => s.parsed ? Object.keys(s.parsed) : null) as any))
+  ).sort(), ['__time'], []);
+
+  if (ignoreColumn) {
+    columns = columns.filter(c => c !== ignoreColumn);
+  }
+
+  return columns;
+}
+
+export function headerAndRowsFromSampleResponse(sampleResponse: SampleResponse, ignoreColumn?: string, parsedOnly = false): HeaderAndRows {
+  return {
+    header: headerFromSampleResponse(sampleResponse, ignoreColumn),
+    rows: parsedOnly ? sampleResponse.data.filter((d: any) => d.parsed) : sampleResponse.data
+  };
+}
+
+async function postToSampler(sampleSpec: SampleSpec, forStr: string): Promise<SampleResponse> {
+  let sampleResp: any;
+  try {
+    sampleResp = await axios.post(`${SAMPLER_URL}?for=${forStr}`, sampleSpec);
+  } catch (e) {
+    throw new Error(getDruidErrorMessage(e));
+  }
+
+  return sampleResp.data;
+}
+
+export async function sampleForConnect(spec: IngestionSpec): Promise<SampleResponse> {
+  const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+
+  const sampleSpec: SampleSpec = {
+    type: 'index',
+    spec: {
+      ioConfig: deepSet(ioConfig, 'type', 'index')
+      // dataSchema: {
+      //   dataSource: 'sample',
+      //   parser: {
+      //     type: 'string',
+      //     parseSpec: {
+      //       format: 'json',
+      //       dimensionsSpec: {},
+      //       timestampSpec: getEmptyTimestampSpec()
+      //     }
+      //   }
+      // }
+    } as any,
+    samplerConfig: BASE_SAMPLER_CONFIG
+  };
+
+  return postToSampler(sampleSpec, 'connect');
+}
+
+export async function sampleForParser(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
+  const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+  const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+
+  const sampleSpec: SampleSpec = {
+    type: 'index',
+    spec: {
+      ioConfig: deepSet(ioConfig, 'type', 'index'),
+      dataSchema: {
+        dataSource: 'sample',
+        parser: {
+          type: parser.type,
+          parseSpec: (
+            parser.parseSpec ?
+              Object.assign({}, parser.parseSpec, {
+                dimensionsSpec: {},
+                timestampSpec: getEmptyTimestampSpec()
+              }) :
+              undefined
+          ) as any
+        }
+      }
+    },
+    samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+      cacheKey
+    })
+  };
+
+  return postToSampler(sampleSpec, 'parser');
+}
+
+export async function sampleForTimestamp(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
+  const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+  const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+  const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+
+  const sampleSpec: SampleSpec = {
+    type: 'index',
+    spec: {
+      ioConfig: deepSet(ioConfig, 'type', 'index'),
+      dataSchema: {
+        dataSource: 'sample',
+        parser: {
+          type: parser.type,
+          parseSpec: Object.assign({}, parseSpec, {
+            dimensionsSpec: {}
+          })
+        }
+      }
+    },
+    samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+      cacheKey
+    })
+  };
+
+  return postToSampler(sampleSpec, 'timestamp');
+}
+
+export async function sampleForTransform(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
+  const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+  const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+  const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+  const transforms: Transform[] = deepGet(spec, 'dataSchema.transformSpec.transforms') || [];
+
+  // Extra step to simulate auto detecting dimension with transforms
+  const specialDimensionSpec: DimensionsSpec = {};
+  if (transforms && transforms.length) {
+
+    const sampleSpecHack: SampleSpec = {
+      type: 'index',
+      spec: {
+        ioConfig: deepSet(ioConfig, 'type', 'index'),
+        dataSchema: {
+          dataSource: 'sample',
+          parser: {
+            type: parser.type,
+            parseSpec: Object.assign({}, parseSpec, {
+              dimensionsSpec: {}
+            })
+          }
+        }
+      },
+      samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+        cacheKey
+      })
+    };
+
+    const sampleResponseHack = await postToSampler(sampleSpecHack, 'transform-pre');
+
+    specialDimensionSpec.dimensions = dedupe(headerFromSampleResponse(sampleResponseHack, '__time').concat(transforms.map(t => t.name)));
+  }
+
+  const sampleSpec: SampleSpec = {
+    type: 'index',
+    spec: {
+      ioConfig: deepSet(ioConfig, 'type', 'index'),
+      dataSchema: {
+        dataSource: 'sample',
+        parser: {
+          type: parser.type,
+          parseSpec: Object.assign({}, parseSpec, {
+            dimensionsSpec: specialDimensionSpec // Hack Hack Hack
+          })
+        },
+        transformSpec: {
+          transforms
+        }
+      }
+    },
+    samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+      cacheKey
+    })
+  };
+
+  return postToSampler(sampleSpec, 'transform');
+}
+
+export async function sampleForFilter(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
+  const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+  const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+  const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+  const transforms: Transform[] = deepGet(spec, 'dataSchema.transformSpec.transforms') || [];
+  const filter: any = deepGet(spec, 'dataSchema.transformSpec.filter');
+
+  // Extra step to simulate auto detecting dimension with transforms
+  const specialDimensionSpec: DimensionsSpec = {};
+  if (transforms && transforms.length) {
+
+    const sampleSpecHack: SampleSpec = {
+      type: 'index',
+      spec: {
+        ioConfig: deepSet(ioConfig, 'type', 'index'),
+        dataSchema: {
+          dataSource: 'sample',
+          parser: {
+            type: parser.type,
+            parseSpec: Object.assign({}, parseSpec, {
+              dimensionsSpec: {}
+            })
+          }
+        }
+      },
+      samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+        cacheKey
+      })
+    };
+
+    const sampleResponseHack = await postToSampler(sampleSpecHack, 'filter-pre');
+
+    specialDimensionSpec.dimensions = dedupe(headerFromSampleResponse(sampleResponseHack, '__time').concat(transforms.map(t => t.name)));
+  }
+
+  const sampleSpec: SampleSpec = {
+    type: 'index',
+    spec: {
+      ioConfig: deepSet(ioConfig, 'type', 'index'),
+      dataSchema: {
+        dataSource: 'sample',
+        parser: {
+          type: parser.type,
+          parseSpec: Object.assign({}, parseSpec, {
+            dimensionsSpec: specialDimensionSpec // Hack Hack Hack
+          })
+        },
+        transformSpec: {
+          transforms,
+          filter
+        }
+      }
+    },
+    samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+      cacheKey
+    })
+  };
+
+  return postToSampler(sampleSpec, 'filter');
+}
+
+export async function sampleForSchema(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
+  const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+  const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+  const transformSpec: TransformSpec = deepGet(spec, 'dataSchema.transformSpec') || ({} as TransformSpec);
+  const metricsSpec: MetricSpec[] = deepGet(spec, 'dataSchema.metricsSpec') || [];
+  const queryGranularity: string = deepGet(spec, 'dataSchema.granularitySpec.queryGranularity') || 'NONE';
+
+  const sampleSpec: SampleSpec = {
+    type: 'index',
+    spec: {
+      ioConfig: deepSet(ioConfig, 'type', 'index'),
+      dataSchema: {
+        dataSource: 'sample',
+        parser: whitelistKeys(parser, ['type', 'parseSpec']) as Parser,
+        transformSpec,
+        metricsSpec,
+        granularitySpec: {
+          queryGranularity
+        }
+      }
+    },
+    samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
+      cacheKey
+    })
+  };
+
+  return postToSampler(sampleSpec, 'schema');
+}
diff --git a/web-console/src/utils/spec-utils.spec.ts b/web-console/src/utils/spec-utils.spec.ts
new file mode 100644
index 0000000..e8ad16d
--- /dev/null
+++ b/web-console/src/utils/spec-utils.spec.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { computeFlattenExprsForData } from './spec-utils';
+
+describe('spec-utils', () => {
+  describe('computeFlattenExprsForData', () => {
+    const data = [
+      {
+        context: {'host': 'clarity', 'topic': 'moon', 'bonus': {'foo': 'bar'}},
+        tags: ['a', 'b', 'c'],
+        messages: [
+          { metric: 'request/time', value: 122 },
+          { metric: 'request/time', value: 434 },
+          { metric: 'request/time', value: 565 }
+        ],
+        'value': 5
+      },
+      {
+        context: {'host': 'pivot', 'popic': 'sun'},
+        tags: ['a', 'd'],
+        messages: [
+          { metric: 'request/time', value: 44 },
+          { metric: 'request/time', value: 65 }
+        ],
+        'value': 4
+      },
+      {
+        context: {'host': 'imply', 'dopik': 'fun'},
+        tags: ['x', 'y'],
+        messages: [
+          { metric: 'request/time', value: 4 },
+          { metric: 'request/time', value: 5 }
+        ],
+        'value': 2
+      }
+    ];
+
+    it('works for path, ignore-arrays', () => {
+      expect(computeFlattenExprsForData(data, 'path', 'ignore-arrays')).toEqual([
+        '$.context.bonus.foo',
+        '$.context.dopik',
+        '$.context.host',
+        '$.context.popic',
+        '$.context.topic'
+      ]);
+    });
+
+    it('works for jq, ignore-arrays', () => {
+      expect(computeFlattenExprsForData(data, 'jq', 'ignore-arrays')).toEqual([
+        '.context.bonus.foo',
+        '.context.dopik',
+        '.context.host',
+        '.context.popic',
+        '.context.topic'
+      ]);
+    });
+
+  });
+
+});
diff --git a/web-console/src/utils/spec-utils.ts b/web-console/src/utils/spec-utils.ts
new file mode 100644
index 0000000..956b809
--- /dev/null
+++ b/web-console/src/utils/spec-utils.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { FlattenField } from './ingestion-spec';
+
+export type ExprType = 'path' | 'jq';
+export type ArrayHandling = 'ignore-arrays' | 'include-arrays';
+
+export function computeFlattenPathsForData(data: Record<string, any>[], exprType: ExprType, arrayHandling: ArrayHandling): FlattenField[] {
+  return computeFlattenExprsForData(data, exprType, arrayHandling).map((expr, i) => {
+    return {
+      type: exprType,
+      name: `expr_${i}`,
+      expr
+    };
+  });
+}
+
+export function computeFlattenExprsForData(data: Record<string, any>[], exprType: ExprType, arrayHandling: ArrayHandling): string[] {
+  const seenPaths: Record<string, boolean> = {};
+  for (const datum of data) {
+    const datumKeys = Object.keys(datum);
+    for (const datumKey of datumKeys) {
+      const datumValue = datum[datumKey];
+      if (isNested(datumValue)) {
+        addPath(seenPaths, (exprType === 'path' ? `$.${datumKey}` : `.${datumKey}`), datumValue, arrayHandling);
+      }
+    }
+  }
+
+  return Object.keys(seenPaths).sort();
+}
+
+function addPath(paths: Record<string, boolean>, path: string, value: any, arrayHandling: ArrayHandling) {
+  if (isNested(value)) {
+    if (!Array.isArray(value)) {
+      const valueKeys = Object.keys(value);
+      for (const valueKey of valueKeys) {
+        addPath(paths, `${path}.${valueKey}`, value[valueKey], arrayHandling);
+      }
+    } else if (arrayHandling === 'include-arrays') {
+      for (let i = 0; i < value.length; i++) {
+        addPath(paths, `${path}[${i}]`, value[i], arrayHandling);
+      }
+    }
+
+  } else {
+    paths[path] = true;
+  }
+}
+
+// Checks that the given value is nested as far as Druid is concerned
+function isNested(v: any): boolean {
+  return Boolean(v) && typeof v === 'object' && (!Array.isArray(v) || v.some(isNested));
+}
diff --git a/web-console/src/variables.ts b/web-console/src/variables.ts
index 10a51ec..05d6473 100644
--- a/web-console/src/variables.ts
+++ b/web-console/src/variables.ts
@@ -23,6 +23,7 @@ export const DRUID_WEBSITE = 'http://druid.io';
 export const DRUID_GITHUB = 'https://github.com/apache/druid';
 export const DRUID_DOCS = 'http://druid.io/docs/latest';
 export const DRUID_DOCS_SQL = 'http://druid.io/docs/latest/querying/sql.html';
+export const DRUID_DOCS_RUNE = 'http://druid.io/docs/latest/querying/querying.html';
 export const DRUID_COMMUNITY = 'http://druid.io/community/';
 export const DRUID_USER_GROUP = 'https://groups.google.com/forum/#!forum/druid-user';
 export const DRUID_DEVELOPER_GROUP = 'https://lists.apache.org/list.html?dev@druid.apache.org';
diff --git a/web-console/src/views/datasource-view.tsx b/web-console/src/views/datasource-view.tsx
index 406599a..e08cd45 100644
--- a/web-console/src/views/datasource-view.tsx
+++ b/web-console/src/views/datasource-view.tsx
@@ -211,7 +211,7 @@ GROUP BY 1`);
         } : null
       }
       confirmButtonText="Drop data"
-      successText="Data has been dropped"
+      successText="Data drop request acknowledged, next time the coordinator runs data will be dropped"
       failText="Could not drop data"
       intent={Intent.DANGER}
       onClose={(success) => {
diff --git a/web-console/src/views/load-data-view.scss b/web-console/src/views/load-data-view.scss
new file mode 100644
index 0000000..050672d
--- /dev/null
+++ b/web-console/src/views/load-data-view.scss
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+
+.load-data-view {
+  height: 100%;
+  display: grid;
+  grid-gap: 10px 15px;
+  grid-template-columns: 1fr 280px;
+  grid-template-rows: 55px 1fr 28px;
+  grid-template-areas:
+    "navi navi"
+    "main ctrl"
+    "main next";
+
+  &.init {
+    display: block;
+
+    & > * {
+      margin-bottom: 15px;
+    }
+
+    .intro {
+      font-size: 20px;
+    }
+
+    .section-title {
+      margin-bottom: 10px;
+      font-weight: bold;
+    }
+
+    .cards {
+      .bp3-card {
+        display: inline-block;
+        width: 250px;
+        height: 140px;
+        margin-right: 15px;
+        margin-bottom: 15px;
+        font-size: 24px;
+        text-align: center;
+        padding-top: 47px;
+      }
+    }
+  }
+
+  &.partition,
+  &.tuning,
+  &.publish {
+    grid-gap: 20px 50px;
+    grid-template-columns: 1fr 1fr 1fr;
+    grid-template-areas:
+      "navi navi navi"
+      "main othr ctrl"
+      "main othr next";
+
+    .main,
+    .other {
+      overflow: auto;
+    }
+  }
+
+  .stage-nav {
+    grid-area: navi;
+    white-space: nowrap;
+    overflow: auto;
+
+    .stage-section {
+      display: inline-block;
+      vertical-align: top;
+      margin-right: 15px;
+    }
+
+    .stage-nav-l1 {
+      height: 25px;
+      font-weight: bold;
+      color: #eeeeee;
+    }
+
+    .stage-nav-l2 {
+      height: 30px;
+    }
+  }
+
+  .main {
+    grid-area: main;
+    position: relative;
+
+    .raw-lines {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      white-space: pre;
+    }
+
+    .table-with-control {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+
+      .table-control {
+        position: absolute;
+        width: 100%;
+        top: 0;
+        height: 30px;
+
+        & > * {
+          display: inline-block;
+          margin-right: 15px;
+        }
+
+        .clearable-input {
+          width: 250px;
+        }
+      }
+
+      .ReactTable {
+        position: absolute;
+        width: 100%;
+        top: 45px;
+        bottom: 0;
+
+        .rt-th {
+          position: relative;
+
+          &.selected::after {
+            position: absolute;
+            top: 0;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            content: "";
+            border: 2px solid #ff5d10;
+            border-radius: 2px;
+          }
+
+          &.flattened,
+          &.transformed {
+            background: rgba(201, 128, 22, 0.2);
+          }
+
+          .clickable {
+            cursor: pointer;
+          }
+
+          &.timestamp {
+            background: rgba(19, 129, 201, 0.5);
+          }
+
+          &.dimension {
+            background: rgba(38, 170, 201, 0.5);
+
+            &.long { background: rgba(19, 129, 201, 0.5); }
+            &.float { background: rgba(25, 145, 201, 0.5); }
+          }
+
+          &.metric {
+            background: rgba(201, 191, 55, 0.5);
+          }
+        }
+
+        .rt-td {
+          &.flattened,
+          &.transformed {
+            background: rgba(201, 128, 22, 0.05);
+          }
+
+          &.timestamp {
+            background: rgba(19, 129, 201, 0.15);
+          }
+
+          &.dimension {
+            background: rgba(38, 170, 201, 0.1);
+
+            &.long { background: rgba(19, 129, 201, 0.1); }
+            &.float { background: rgba(25, 145, 201, 0.1); }
+          }
+
+          &.metric {
+            background: rgba(201, 191, 55, 0.1);
+          }
+        }
+
+        .parse-detail {
+          padding: 10px;
+
+          .parse-error {
+            color: #9E2B0E;
+            margin-bottom: 12px;
+          }
+        }
+      }
+    }
+  }
+
+  .other {
+    grid-area: othr;
+  }
+
+  .control {
+    grid-area: ctrl;
+
+    .intro {
+      margin-bottom: 15px;
+
+      .optional {
+        font-style: italic;
+      }
+    }
+  }
+
+  .next-bar {
+    grid-area: next;
+    text-align: right;
+
+    .prev {
+      float: left;
+    }
+  }
+
+  .column-name {
+    font-weight: bold;
+  }
+
+  .edit-controls {
+    background: #30404c;
+    padding: 10px;
+    border-radius: 2px;
+    margin-bottom: 15px;
+
+    .controls-buttons {
+      position: relative;
+
+      .add-update {
+        margin-right: 15px;
+      }
+
+      .cancel {
+        position: absolute;
+        right: 0;
+      }
+    }
+  }
+}
diff --git a/web-console/src/views/load-data-view.tsx b/web-console/src/views/load-data-view.tsx
new file mode 100644
index 0000000..22a0dbb
--- /dev/null
+++ b/web-console/src/views/load-data-view.tsx
@@ -0,0 +1,2353 @@
+/*
+ * 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 {
+  Alert,
+  AnchorButton,
+  Button,
+  ButtonGroup, Callout, Card,
+  Classes, Code,
+  FormGroup, H5,
+  Icon, Intent, Popover, Switch, TextArea
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import 'brace/mode/json';
+import 'brace/theme/solarized_dark';
+import * as classNames from 'classnames';
+import * as React from 'react';
+import ReactTable from 'react-table';
+
+import { AutoForm } from '../components/auto-form';
+import { CenterMessage } from '../components/center-message';
+import { ClearableInput } from '../components/clearable-input';
+import { ExternalLink } from '../components/external-link';
+import { JSONInput } from '../components/json-input';
+import { Loader } from '../components/loader';
+import { NullTableCell } from '../components/null-table-cell';
+import { AsyncActionDialog } from '../dialogs/async-action-dialog';
+import { AppToaster } from '../singletons/toaster';
+import {
+  filterMap,
+  getDruidErrorMessage,
+  localStorageGet,
+  LocalStorageKeys,
+  localStorageSet, parseJson,
+  QueryState, sortWithPrefixSuffix
+} from '../utils';
+import { escapeColumnName } from '../utils/druid-expression';
+import { possibleDruidFormatForValues } from '../utils/druid-time';
+import { updateSchemaWithSample } from '../utils/druid-type';
+import {
+  changeParallel, DimensionMode,
+  DimensionSpec, DimensionsSpec, DruidFilter,
+  fillDataSourceName,
+  fillParser,
+  FlattenField, getBlankSpec, getDimensionMode,
+  getDimensionSpecFormFields,
+  getDimensionSpecName, getDimensionSpecType, getEmptyTimestampSpec, getFilterFormFields, getFlattenFieldFormFields,
+  getIngestionComboType, getIoConfigFormFields, getIoConfigTuningFormFields, getMetricSpecFormFields,
+  getMetricSpecName, getParseSpecFormFields, getRollup, getTimestampSpecColumn, getTimestampSpecFormFields,
+  getTransformFormFields,
+  getTuningSpecFormFields, GranularitySpec, hasParallelAbility, inflateDimensionSpec, IngestionSpec,
+  IngestionType, IoConfig,
+  isColumnTimestampSpec, isParallel, issueWithIoConfig, issueWithParser, joinFilter,
+  MetricSpec, Parser, ParseSpec,
+  parseSpecHasFlatten, splitFilter, TimestampSpec, Transform, TuningConfig
+} from '../utils/ingestion-spec';
+import { deepDelete, deepGet, deepSet } from '../utils/object-change';
+import {
+  HeaderAndRows,
+  headerAndRowsFromSampleResponse,
+  SampleEntry,
+  sampleForConnect,
+  sampleForFilter,
+  sampleForParser, sampleForSchema,
+  sampleForTimestamp,
+  sampleForTransform,
+  SampleResponse
+} from '../utils/sampler';
+import { computeFlattenPathsForData } from '../utils/spec-utils';
+
+import './load-data-view.scss';
+
+export interface LoadDataViewSeed {
+  type?: IngestionType;
+  firehoseType?: string;
+  initSpec?: IngestionSpec;
+}
+
+function filterMatch(testString: string, searchString: string): boolean {
+  if (!searchString) return true;
+  return testString.toLowerCase().includes(searchString.toLowerCase());
+}
+
+function getTimestampSpec(headerAndRows: HeaderAndRows | null): TimestampSpec {
+  if (!headerAndRows) return getEmptyTimestampSpec();
+
+  const timestampSpecs = headerAndRows.header.map(sampleHeader => {
+    const possibleFormat = possibleDruidFormatForValues(filterMap(headerAndRows.rows, d => d.parsed ? d.parsed[sampleHeader] : null));
+    if (!possibleFormat) return null;
+    return {
+      column: sampleHeader,
+      format: possibleFormat
+    };
+  }).filter(Boolean);
+
+  return timestampSpecs[0] || getEmptyTimestampSpec();
+}
+
+type Stage = 'connect' | 'parser' | 'timestamp' | 'transform' | 'filter' | 'schema' | 'partition' | 'tuning' | 'publish' | 'json-spec';
+const STAGES: Stage[] = ['connect', 'parser', 'timestamp', 'transform', 'filter', 'schema', 'partition', 'tuning', 'publish', 'json-spec'];
+
+const SECTIONS: { name: string, stages: Stage[] }[] = [
+  { name: 'Connect and parse raw data', stages: ['connect', 'parser', 'timestamp'] },
+  { name: 'Transform and configure schema', stages: ['transform', 'filter', 'schema'] },
+  { name: 'Tune parameters', stages: ['partition', 'tuning', 'publish'] },
+  { name: 'Verify and submit', stages: ['json-spec'] }
+];
+
+const VIEW_TITLE: Record<Stage, string> = {
+  'connect': 'Connect',
+  'parser': 'Parse data',
+  'timestamp': 'Parse time',
+  'transform': 'Transform',
+  'filter': 'Filter',
+  'schema': 'Configure schema',
+  'partition': 'Partition',
+  'tuning': 'Tune',
+  'publish': 'Publish',
+  'json-spec': 'Edit JSON spec'
+};
+
+export interface LoadDataViewProps extends React.Props<any> {
+  seed: LoadDataViewSeed | null;
+  goToTask: (taskId: string | null) => void;
+}
+
+export interface LoadDataViewState {
+  stage: Stage;
+  spec: IngestionSpec;
+  cacheKey: string | undefined;
+
+  // dialogs / modals
+  showResetConfirm: boolean;
+  newRollup: boolean | null;
+  newDimensionMode: DimensionMode | null;
+
+  // general
+  columnFilter: string;
+  specialColumnsOnly: boolean;
+
+  // for ioConfig
+  inputQueryState: QueryState<string[]>;
+
+  // for parser
+  parserQueryState: QueryState<HeaderAndRows>;
+
+  // for flatten
+  flattenQueryState: QueryState<HeaderAndRows>;
+  selectedFlattenFieldIndex: number;
+  selectedFlattenField: FlattenField | null;
+
+  // for timestamp
+  timestampQueryState: QueryState<HeaderAndRows>;
+
+  // for transform
+  transformQueryState: QueryState<HeaderAndRows>;
+  selectedTransformIndex: number;
+  selectedTransform: Transform | null;
+
+  // for filter
+  filterQueryState: QueryState<HeaderAndRows>;
+  selectedFilterIndex: number;
+  selectedFilter: DruidFilter | null;
+  showGlobalFilter: boolean;
+
+  // for schema
+  schemaQueryState: QueryState<HeaderAndRows>;
+  selectedDimensionSpecIndex: number;
+  selectedDimensionSpec: DimensionSpec | null;
+  selectedMetricSpecIndex: number;
+  selectedMetricSpec: MetricSpec | null;
+}
+
+export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataViewState> {
+  constructor(props: LoadDataViewProps) {
+    super(props);
+
+    let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
+    if (!spec || typeof spec !== 'object') spec = {};
+
+    this.state = {
+      stage: 'connect',
+      spec,
+      cacheKey: undefined,
+
+      // dialogs / modals
+      showResetConfirm: false,
+      newRollup: null,
+      newDimensionMode: null,
+
+      // general
+      columnFilter: '',
+      specialColumnsOnly: false,
+
+      // for firehose
+      inputQueryState: QueryState.INIT,
+
+      // for parser
+      parserQueryState: QueryState.INIT,
+
+      // for flatten
+      flattenQueryState: QueryState.INIT,
+      selectedFlattenFieldIndex: -1,
+      selectedFlattenField: null,
+
+      // for timestamp
+      timestampQueryState: QueryState.INIT,
+
+      // for transform
+      transformQueryState: QueryState.INIT,
+      selectedTransformIndex: -1,
+      selectedTransform: null,
+
+      // for filter
+      filterQueryState: QueryState.INIT,
+      selectedFilterIndex: -1,
+      selectedFilter: null,
+      showGlobalFilter: false,
+
+      // for dimensions
+      schemaQueryState: QueryState.INIT,
+      selectedDimensionSpecIndex: -1,
+      selectedDimensionSpec: null,
+      selectedMetricSpecIndex: -1,
+      selectedMetricSpec: null
+    };
+  }
+
+  componentDidMount(): void {
+    this.updateStage('connect');
+  }
+
+  private updateStage = (newStage: Stage) => {
+    this.doQueryForStage(newStage);
+    this.setState({ stage: newStage });
+  }
+
+  doQueryForStage(stage: Stage): any {
+    switch (stage) {
+      case 'connect': return this.queryForConnect(true);
+      case 'parser': return this.queryForParser(true);
+      case 'timestamp': return this.queryForTimestamp(true);
+      case 'transform': return this.queryForTransform(true);
+      case 'filter': return this.queryForFilter(true);
+      case 'schema': return this.queryForSchema(true);
+    }
+  }
+
+  private updateSpec = (newSpec: IngestionSpec) => {
+    if (!newSpec || typeof newSpec !== 'object') {
+      // This does not match the type of IngestionSpec but this dialog is robust enough to deal with anything but spec must be an object
+      newSpec = {} as any;
+    }
+    this.setState({ spec: newSpec });
+    localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(newSpec));
+  }
+
+  render() {
+    const { stage, spec } = this.state;
+
+    if (!Object.keys(spec).length) {
+      return <div className={classNames('load-data-view', 'app-view', 'init')}>
+        {this.renderInitStage()}
+      </div>;
+    }
+
+    return <div className={classNames('load-data-view', 'app-view', stage)}>
+      {this.renderStepNav()}
+
+      {stage === 'connect' && this.renderConnectStage()}
+      {stage === 'parser' && this.renderParserStage()}
+      {stage === 'timestamp' && this.renderTimestampStage()}
+
+      {stage === 'transform' && this.renderTransformStage()}
+      {stage === 'filter' && this.renderFilterStage()}
+      {stage === 'schema' && this.renderSchemaStage()}
+
+      {stage === 'partition' && this.renderPartitionStage()}
+      {stage === 'tuning' && this.renderTuningStage()}
+      {stage === 'publish' && this.renderPublishStage()}
+
+      {stage === 'json-spec' && this.renderJsonSpecStage()}
+
+      {this.renderResetConfirm()}
+    </div>;
+  }
+
+  renderStepNav() {
+    const { stage } = this.state;
+
+    return <div className={classNames(Classes.TABS, 'stage-nav')}>
+      {SECTIONS.map(section => (
+        <div className="stage-section" key={section.name}>
+          <div className="stage-nav-l1">
+            {section.name}
+          </div>
+          <ButtonGroup className="stage-nav-l2">
+            {section.stages.map((s) => (
+              <Button
+                className={s}
+                key={s}
+                active={s === stage}
+                onClick={() => this.updateStage(s)}
+                icon={s === 'json-spec' && IconNames.EYE_OPEN}
+                text={VIEW_TITLE[s]}
+              />
+            ))}
+          </ButtonGroup>
+        </div>
+      ))}
+    </div>;
+  }
+
+  renderNextBar(options: { nextStage?: Stage, disabled?: boolean; onNextStage?: () => void, onPrevStage?: () => void, prevLabel?: string }) {
+    const { disabled, onNextStage, onPrevStage, prevLabel } = options;
+    const { stage } = this.state;
+    const nextStage = options.nextStage || STAGES[STAGES.indexOf(stage) + 1] || STAGES[0];
+
+    return <div className="next-bar">
+      {
+        onPrevStage &&
+        <Button
+          className="prev"
+          icon={IconNames.ARROW_LEFT}
+          text={prevLabel}
+          onClick={onPrevStage}
+        />
+      }
+      <Button
+        text={`Next: ${VIEW_TITLE[nextStage]}`}
+        intent={Intent.PRIMARY}
+        disabled={disabled}
+        onClick={() => {
+          if (disabled) return;
+          if (onNextStage) onNextStage();
+
+          setTimeout(() => {
+            this.updateStage(nextStage);
+          }, 10);
+        }}
+      />
+    </div>;
+  }
+
+  // ==================================================================
+
+  initWith(seed: LoadDataViewSeed) {
+    this.setState({
+      spec: getBlankSpec(seed.type, seed.firehoseType)
+    });
+    setTimeout(() => {
+      this.updateStage('connect');
+    }, 10);
+  }
+
+  renderInitStage() {
+    const showStreaming = false;
+
+    return <>
+      <div className="intro">
+        Please specify where your raw data is located
+      </div>
+
+      <Callout intent={Intent.SUCCESS} icon={IconNames.INFO_SIGN}>
+        Welcome to the Druid data loader.
+        This project is under active development and we plan to support many other sources of raw data, including stream hubs such as Apache Kafka and AWS Kinesis, in the next few releases.
+      </Callout>
+
+      {
+        showStreaming &&
+        <div className="section">
+          <div className="section-title">Stream hub</div>
+          <div className="cards">
+            <Card interactive onClick={() => this.initWith({ type: 'kafka' })}>Apache Kafka</Card>
+            <Card interactive onClick={() => this.initWith({ type: 'kinesis' })}>AWS Kinesis</Card>
+          </div>
+        </div>
+      }
+
+      <div className="section">
+        <div className="section-title">Batch load</div>
+        <div className="cards">
+          <Card interactive onClick={() => this.initWith({ type: 'index_parallel', firehoseType: 'http' })}>HTTP(s)</Card>
+          <Card interactive onClick={() => this.initWith({ type: 'index_parallel', firehoseType: 'static-s3' })}>AWS S3</Card>
+          <Card interactive onClick={() => this.initWith({ type: 'index_parallel', firehoseType: 'static-google-blobstore' })}>Google Blobstore</Card>
+          <Card interactive onClick={() => this.initWith({ type: 'index_parallel', firehoseType: 'local' })}>Local disk</Card>
+        </div>
+      </div>
+    </>;
+  }
+
+  renderResetConfirm() {
+    const { showResetConfirm } = this.state;
+    if (!showResetConfirm) return null;
+
+    return <Alert
+      cancelButtonText="Cancel"
+      confirmButtonText="Reset spec"
+      icon="trash"
+      intent={Intent.DANGER}
+      isOpen
+      onCancel={() => this.setState({ showResetConfirm: false })}
+      onConfirm={() => {
+        this.setState({ showResetConfirm: false });
+        this.updateSpec({} as any);
+      }}
+    >
+      <p>
+        This will discard the current progress in the spec.
+      </p>
+    </Alert>;
+  }
+
+  // ==================================================================
+
+  async queryForConnect(initRun = false) {
+    const { spec } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+
+    let issue: string | undefined;
+    if (issueWithIoConfig(ioConfig)) {
+      issue = `IoConfig not ready, ${issueWithIoConfig(ioConfig)}`;
+    }
+
+    if (issue) {
+      this.setState({
+        inputQueryState: initRun ? QueryState.INIT : new QueryState({ error: issue })
+      });
+      return;
+    }
+
+    this.setState({
+      inputQueryState: new QueryState({ loading: true })
+    });
+
+    let sampleResponse: SampleResponse;
+    try {
+      sampleResponse = await sampleForConnect(spec);
+    } catch (e) {
+      this.setState({
+        inputQueryState: new QueryState({ error: e.message })
+      });
+      return;
+    }
+
+    this.setState({
+      cacheKey: sampleResponse.cacheKey,
+      inputQueryState: new QueryState({ data: sampleResponse.data.map((d: any) => d.raw) })
+    });
+  }
+
+  renderConnectStage() {
+    const { spec, inputQueryState } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const isBlank = !ioConfig.type;
+
+    let mainFill: JSX.Element | string = '';
+    if (inputQueryState.isInit()) {
+      mainFill = <CenterMessage>
+        Please fill out the fields on the right sidebar to get started.
+      </CenterMessage>;
+
+    } else if (inputQueryState.isLoading()) {
+      mainFill = <Loader loading/>;
+
+    } else if (inputQueryState.error) {
+      mainFill = <CenterMessage>
+        {`Error: ${inputQueryState.error}`}
+      </CenterMessage>;
+
+    } else if (inputQueryState.data) {
+      const inputData = inputQueryState.data;
+      mainFill = <TextArea
+        className="raw-lines"
+        value={(inputData.every(l => !l) ? inputData.map(_ => '[Binary data]') : inputData).join('\n')}
+        readOnly
+      />;
+    }
+
+    const ingestionComboType = getIngestionComboType(spec);
+    return <>
+      <div className="main">{mainFill}</div>
+      <div className="control">
+        <Callout className="intro">
+          <p>
+            Druid ingests raw data and converts it into a custom, <ExternalLink href="http://druid.io/docs/latest/design/segments.html">indexed</ExternalLink> format that is optimized for analytic queries.
+          </p>
+          <p>
+            To get started, please specify where your raw data is stored and what data you want to ingest.
+          </p>
+          <p>
+            Click "Preview" to look at the sampled raw data.
+          </p>
+        </Callout>
+        {
+          ingestionComboType ?
+          <AutoForm
+            fields={getIoConfigFormFields(ingestionComboType)}
+            model={ioConfig}
+            onChange={c => this.updateSpec(deepSet(spec, 'ioConfig', c))}
+          /> :
+          <FormGroup label="IO Config">
+            <JSONInput
+              value={ioConfig}
+              onChange={c => this.updateSpec(deepSet(spec, 'ioConfig', c))}
+              height="300px"
+            />
+          </FormGroup>
+        }
+        {
+          deepGet(spec, 'ioConfig.firehose.type') === 'local' &&
+          <FormGroup>
+            <Callout intent={Intent.WARNING}>
+              This path must be available on the local filesystem of all Druid servers.
+            </Callout>
+          </FormGroup>
+        }
+        <Button
+          text="Preview"
+          disabled={isBlank}
+          onClick={() => this.queryForConnect()}
+        />
+      </div>
+      {this.renderNextBar({
+        disabled: !inputQueryState.data,
+        onNextStage: () => {
+          if (!inputQueryState.data) return;
+          this.updateSpec(fillDataSourceName(fillParser(spec, inputQueryState.data)));
+        },
+        prevLabel: 'Restart',
+        onPrevStage: () => this.setState({ showResetConfirm: true })
+      })}
+    </>;
+  }
+
+  // ==================================================================
+
+  async queryForParser(initRun = false) {
+    const { spec, cacheKey } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+
+    let issue: string | null = null;
+    if (issueWithIoConfig(ioConfig)) {
+      issue = `IoConfig not ready, ${issueWithIoConfig(ioConfig)}`;
+    } else if (issueWithParser(parser)) {
+      issue = `Parser not ready, ${issueWithParser(parser)}`;
+    }
+
+    if (issue) {
+      this.setState({
+        parserQueryState: initRun ? QueryState.INIT : new QueryState({ error: issue })
+      });
+      return;
+    }
+
+    this.setState({
+      parserQueryState: new QueryState({ loading: true })
+    });
+
+    let sampleResponse: SampleResponse;
+    try {
+      sampleResponse = await sampleForParser(spec, cacheKey);
+    } catch (e) {
+      this.setState({
+        parserQueryState: new QueryState({ error: e.message })
+      });
+      return;
+    }
+
+    this.setState({
+      cacheKey: sampleResponse.cacheKey,
+      parserQueryState: new QueryState({
+        data: headerAndRowsFromSampleResponse(sampleResponse, '__time')
+      })
+    });
+  }
+
+  renderParserStage() {
+    const { spec, columnFilter, specialColumnsOnly, parserQueryState, selectedFlattenField } = this.state;
+    const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+    const flattenFields: FlattenField[] = deepGet(spec, 'dataSchema.parser.parseSpec.flattenSpec.fields') || [];
+
+    const isBlank = !parseSpec.format;
+    const canFlatten = parseSpec.format === 'json';
+
+    let mainFill: JSX.Element | string = '';
+    if (parserQueryState.isInit()) {
+      mainFill = <CenterMessage>
+        Please enter the parser details on the right
+      </CenterMessage>;
+
+    } else if (parserQueryState.isLoading()) {
+      mainFill = <Loader loading/>;
+
+    } else if (parserQueryState.error) {
+      mainFill = <CenterMessage>
+        {`Error: ${parserQueryState.error}`}
+      </CenterMessage>;
+
+    } else if (parserQueryState.data) {
+      mainFill = <div className="table-with-control">
+        <div className="table-control">
+          <ClearableInput
+            value={columnFilter}
+            onChange={(columnFilter) => this.setState({ columnFilter })}
+            placeholder="Search columns"
+          />
+          {
+            canFlatten &&
+            <Switch
+              checked={specialColumnsOnly}
+              label="Flattened columns only"
+              onChange={() => this.setState({ specialColumnsOnly: !specialColumnsOnly })}
+              disabled={!flattenFields.length}
+            />
+          }
+        </div>
+        <ReactTable
+          data={parserQueryState.data.rows}
+          columns={filterMap(parserQueryState.data.header, (columnName, i) => {
+            if (!filterMatch(columnName, columnFilter)) return null;
+            const flattenFieldIndex = flattenFields.findIndex(f => f.name === columnName);
+            if (flattenFieldIndex === -1 && specialColumnsOnly) return null;
+            const flattenField = flattenFields[flattenFieldIndex];
+            return {
+              Header: (
+                <div
+                  className={classNames({ clickable: flattenField })}
+                  onClick={() => {
+                    this.setState({
+                      selectedFlattenFieldIndex: flattenFieldIndex,
+                      selectedFlattenField: flattenField
+                    });
+                  }}
+                >
+                  <div className="column-name">{columnName}</div>
+                  <div className="column-detail">
+                    {flattenField ? `${flattenField.type}: ${flattenField.expr}` : ''}&nbsp;
+                  </div>
+                </div>
+              ),
+              id: String(i),
+              accessor: (row: SampleEntry) => row.parsed ? row.parsed[columnName] : null,
+              Cell: row => {
+                if (row.original.unparseable) {
+                  return <NullTableCell unparseable/>;
+                }
+                return <NullTableCell value={row.value}/>;
+              },
+              headerClassName: classNames({
+                flattened: flattenField
+              })
+            };
+          })}
+          SubComponent={rowInfo => {
+            const { raw, error } = rowInfo.original;
+            const parsedJson: any = parseJson(raw);
+
+            if (!error && parsedJson && canFlatten) {
+              return <pre className="parse-detail">
+                {'Original row: ' + JSON.stringify(parsedJson, null, 2)}
+              </pre>;
+            } else {
+              return <div className="parse-detail">
+                {error && <div className="parse-error">{error}</div>}
+                <div>{'Original row: ' + rowInfo.original.raw}</div>
+              </div>;
+            }
+          }}
+          defaultPageSize={50}
+          showPagination={false}
+          sortable={false}
+          className="-striped -highlight"
+        />
+      </div>;
+    }
+
+    let sugestedFlattenFields: FlattenField[] | null = null;
+    if (canFlatten && !flattenFields.length && parserQueryState.data) {
+      sugestedFlattenFields = computeFlattenPathsForData(filterMap(parserQueryState.data.rows, r => parseJson(r.raw)), 'path', 'ignore-arrays');
+    }
+
+    return <>
+      <div className="main">{mainFill}</div>
+      <div className="control">
+        <Callout className="intro">
+          <p>
+            Druid requires flat data (non-nested, non-hierarchical).
+            Each row should represent a discrete event.
+          </p>
+          {
+            canFlatten &&
+            <p>
+              If you have nested data, you can <ExternalLink href="http://druid.io/docs/latest/ingestion/flatten-json.html">flatten</ExternalLink> it here.
+              If the provided flattening capabilities are not sufficient, please pre-process your data before ingesting it into Druid.
+            </p>
+          }
+          <p>
+            Click "Preview" to ensure that your data appears correctly in a row/column orientation.
+          </p>
+        </Callout>
+        <AutoForm
+          fields={getParseSpecFormFields()}
+          model={parseSpec}
+          onChange={p => this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec', p))}
+        />
+        {this.renderFlattenControls()}
+        {
+          Boolean(sugestedFlattenFields && sugestedFlattenFields.length) &&
+          <FormGroup>
+            <Button
+              icon={IconNames.LIGHTBULB}
+              text="Auto add flatten specs"
+              onClick={() => {
+                this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec.flattenSpec.fields', sugestedFlattenFields));
+                setTimeout(() => {
+                  this.queryForParser();
+                }, 10);
+              }}
+            />
+          </FormGroup>
+        }
+        {
+          !selectedFlattenField &&
+          <Button
+            text="Preview"
+            disabled={isBlank}
+            onClick={() => this.queryForParser()}
+          />
+        }
+      </div>
+      {this.renderNextBar({
+        disabled: !parserQueryState.data,
+        onNextStage: () => {
+          if (!parserQueryState.data) return;
+          const possibleTimestampSpec = getTimestampSpec(parserQueryState.data);
+          if (possibleTimestampSpec) {
+            const newSpec: IngestionSpec = deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', possibleTimestampSpec);
+            this.updateSpec(newSpec);
+          }
+        }
+      })}
+    </>;
+  }
+
+  renderFlattenControls() {
+    const { spec, selectedFlattenField, selectedFlattenFieldIndex } = this.state;
+    const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+    if (!parseSpecHasFlatten(parseSpec)) return null;
+
+    const close = () => {
+      this.setState({
+        selectedFlattenFieldIndex: -1,
+        selectedFlattenField: null
+      });
+    };
+
+    const closeAndQuery = () => {
+      close();
+      setTimeout(() => {
+        this.queryForParser();
+      }, 10);
+    };
+
+    if (selectedFlattenField) {
+      return <div className="edit-controls">
+        <AutoForm
+          fields={getFlattenFieldFormFields()}
+          model={selectedFlattenField}
+          onChange={(f) => this.setState({ selectedFlattenField: f })}
+        />
+        <div className="controls-buttons">
+          <Button
+            className="add-update"
+            text={selectedFlattenFieldIndex === -1 ? 'Add' : 'Update'}
+            intent={Intent.PRIMARY}
+            onClick={() => {
+              this.updateSpec(deepSet(spec, `dataSchema.parser.parseSpec.flattenSpec.fields.${selectedFlattenFieldIndex}`, selectedFlattenField));
+              closeAndQuery();
+            }}
+          />
+          {
+            selectedFlattenFieldIndex !== -1 &&
+            <Button
+              icon={IconNames.TRASH}
+              intent={Intent.DANGER}
+              onClick={() => {
+                this.updateSpec(deepDelete(spec, `dataSchema.parser.parseSpec.flattenSpec.fields.${selectedFlattenFieldIndex}`));
+                closeAndQuery();
+              }}
+            />
+          }
+          <Button className="cancel" text="Cancel" onClick={close}/>
+        </div>
+      </div>;
+    } else {
+      return <FormGroup>
+        <Button
+          text="Add column flattening"
+          onClick={() => {
+            this.setState({
+              selectedFlattenField: { type: 'path', name: '', expr: '' },
+              selectedFlattenFieldIndex: -1
+            });
+          }}
+        />
+        <AnchorButton
+          icon={IconNames.INFO_SIGN}
+          href="http://druid.io/docs/latest/ingestion/flatten-json.html"
+          target="_blank"
+          minimal
+        />
+      </FormGroup>;
+    }
+  }
+
+  // ==================================================================
+
+  async queryForTimestamp(initRun = false) {
+    const { spec, cacheKey } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+
+    let issue: string | null = null;
+    if (issueWithIoConfig(ioConfig)) {
+      issue = `IoConfig not ready, ${issueWithIoConfig(ioConfig)}`;
+    } else if (issueWithParser(parser)) {
+      issue = `Parser not ready, ${issueWithParser(parser)}`;
+    }
+
+    if (issue) {
+      this.setState({
+        timestampQueryState: initRun ? QueryState.INIT : new QueryState({ error: issue })
+      });
+      return;
+    }
+
+    this.setState({
+      timestampQueryState: new QueryState({ loading: true })
+    });
+
+    let sampleResponse: SampleResponse;
+    try {
+      sampleResponse = await sampleForTimestamp(spec, cacheKey);
+    } catch (e) {
+      this.setState({
+        timestampQueryState: new QueryState({ error: e.message })
+      });
+      return;
+    }
+
+    this.setState({
+      cacheKey: sampleResponse.cacheKey,
+      timestampQueryState: new QueryState({
+        data: headerAndRowsFromSampleResponse(sampleResponse)
+      })
+    });
+  }
+
+  renderTimestampStage() {
+    const { spec, columnFilter, specialColumnsOnly, timestampQueryState } = this.state;
+    const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+    const timestampSpec: TimestampSpec = deepGet(spec, 'dataSchema.parser.parseSpec.timestampSpec') || {};
+    const timestampSpecColumn = getTimestampSpecColumn(timestampSpec);
+    const timestampSpecFromColumn = isColumnTimestampSpec(timestampSpec);
+
+    const isBlank = !parseSpec.format;
+
+    let mainFill: JSX.Element | string = '';
+    if (timestampQueryState.isInit()) {
+      mainFill = <CenterMessage>
+        Please enter the timestamp column details on the right
+      </CenterMessage>;
+
+    } else  if (timestampQueryState.isLoading()) {
+      mainFill = <Loader loading/>;
+
+    } else if (timestampQueryState.error) {
+      mainFill = <CenterMessage>
+        {`Error: ${timestampQueryState.error}`}
+      </CenterMessage>;
+
+    } else if (timestampQueryState.data) {
+      const timestampData = timestampQueryState.data;
+      mainFill = <div className="table-with-control">
+        <div className="table-control">
+          <ClearableInput
+            value={columnFilter}
+            onChange={(columnFilter) => this.setState({ columnFilter })}
+            placeholder="Search columns"
+          />
+          <Switch
+            checked={specialColumnsOnly}
+            label="Suggested columns only"
+            onChange={() => this.setState({ specialColumnsOnly: !specialColumnsOnly })}
+          />
+        </div>
+        <ReactTable
+          data={timestampData.rows}
+          columns={filterMap(timestampData.header.length ? timestampData.header : ['__error__'], (columnName, i) => {
+            const timestamp = columnName === '__time';
+            if (!timestamp && !filterMatch(columnName, columnFilter)) return null;
+            const selected = timestampSpec.column === columnName;
+            const possibleFormat = timestamp ? null : possibleDruidFormatForValues(filterMap(timestampData.rows, d => d.parsed ? d.parsed[columnName] : null));
+            if (specialColumnsOnly && !timestamp && !possibleFormat) return null;
+
+            const columnClassName = classNames({
+              timestamp,
+              selected
+            });
+            return {
+              Header: (
+                <div
+                  className={classNames({ clickable: !timestamp })}
+                  onClick={timestamp ? undefined : () => {
+                    const newTimestampSpec = {
+                      column: columnName,
+                      format: possibleFormat || '!!! Could not auto detect a format !!!'
+                    };
+                    this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', newTimestampSpec));
+                  }}
+                >
+                  <div className="column-name">{columnName}</div>
+                  <div className="column-detail">
+                    {
+                      timestamp ?
+                        (timestampSpecFromColumn ? `from: '${timestampSpecColumn}'` : `mv: ${timestampSpec.missingValue}`) :
+                        (possibleFormat || '')
+                    }&nbsp;
+                  </div>
+                </div>
+              ),
+              headerClassName: columnClassName,
+              className: columnClassName,
+              id: String(i),
+              accessor: (row: SampleEntry) => row.parsed ? row.parsed[columnName] : null,
+              Cell: row => {
+                if (columnName === '__error__') {
+                  return <NullTableCell value={row.original.error}/>;
+                }
+                if (row.original.unparseable) {
+                  return <NullTableCell unparseable/>;
+                }
+                return <NullTableCell value={row.value} timestamp={timestamp}/>;
+              },
+              minWidth: timestamp ? 200 : 100,
+              resizable: !timestamp
+          };
+          })}
+          defaultPageSize={50}
+          showPagination={false}
+          sortable={false}
+          className="-striped -highlight"
+        />
+      </div>;
+    }
+
+    return <>
+      <div className="main">{mainFill}</div>
+      <div className="control">
+        <Callout className="intro">
+          <p>
+            Druid partitions data based on the primary time column of your data.
+            This column is stored internally in Druid as <Code>__time</Code>.
+            Please specify the primary time column.
+            If you do not have any time columns, you can choose "Constant Value" to create a default one.
+          </p>
+          <p>
+            Click "Preview" to check if Druid can properly parse your time values.
+          </p>
+        </Callout>
+        <FormGroup label="Timestamp spec">
+          <ButtonGroup>
+            <Button
+              text="From column"
+              active={timestampSpecFromColumn}
+              onClick={() => {
+                const timestampSpec = {
+                  column: 'timestamp',
+                  format: 'auto'
+                };
+                this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', timestampSpec));
+                setTimeout(() => {
+                  this.queryForTimestamp();
+                }, 10);
+              }}
+            />
+            <Button
+              text="Constant value"
+              active={!timestampSpecFromColumn}
+              onClick={() => {
+                this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', getEmptyTimestampSpec()));
+                setTimeout(() => {
+                  this.queryForTimestamp();
+                }, 10);
+              }}
+            />
+          </ButtonGroup>
+        </FormGroup>
+        <AutoForm
+          fields={getTimestampSpecFormFields()}
+          model={timestampSpec}
+          onChange={(timestampSpec) => {
+            this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', timestampSpec));
+          }}
+        />
+        <Button
+          text="Preview"
+          disabled={isBlank}
+          onClick={() => this.queryForTimestamp()}
+        />
+      </div>
+      {this.renderNextBar({
+        disabled: !timestampQueryState.data
+      })}
+    </>;
+  }
+
+  // ==================================================================
+
+  async queryForTransform(initRun = false) {
+    const { spec, cacheKey } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+
+    let issue: string | null = null;
+    if (issueWithIoConfig(ioConfig)) {
+      issue = `IoConfig not ready, ${issueWithIoConfig(ioConfig)}`;
+    } else if (issueWithParser(parser)) {
+      issue = `Parser not ready, ${issueWithParser(parser)}`;
+    }
+
+    if (issue) {
+      this.setState({
+        transformQueryState: initRun ? QueryState.INIT : new QueryState({ error: issue })
+      });
+      return;
+    }
+
+    this.setState({
+      transformQueryState: new QueryState({ loading: true })
+    });
+
+    let sampleResponse: SampleResponse;
+    try {
+      sampleResponse = await sampleForTransform(spec, cacheKey);
+    } catch (e) {
+      this.setState({
+        transformQueryState: new QueryState({ error: e.message })
+      });
+      return;
+    }
+
+    this.setState({
+      cacheKey: sampleResponse.cacheKey,
+      transformQueryState: new QueryState({
+        data: headerAndRowsFromSampleResponse(sampleResponse)
+      })
+    });
+  }
+
+  renderTransformStage() {
+    const { spec, columnFilter, specialColumnsOnly, transformQueryState, selectedTransformIndex } = this.state;
+    const transforms: Transform[] = deepGet(spec, 'dataSchema.transformSpec.transforms') || [];
+
+    let mainFill: JSX.Element | string = '';
+    if (transformQueryState.isInit()) {
+      mainFill = <CenterMessage>
+        {`Please fill in the previous steps`}
+      </CenterMessage>;
+
+    } else  if (transformQueryState.isLoading()) {
+      mainFill = <Loader loading/>;
+
+    } else if (transformQueryState.error) {
+      mainFill = <CenterMessage>
+        {`Error: ${transformQueryState.error}`}
+      </CenterMessage>;
+
+    } else if (transformQueryState.data) {
+      mainFill = <div className="table-with-control">
+        <div className="table-control">
+          <ClearableInput
+            value={columnFilter}
+            onChange={(columnFilter) => this.setState({ columnFilter })}
+            placeholder="Search columns"
+          />
+          <Switch
+            checked={specialColumnsOnly}
+            label="Transformed columns only"
+            onChange={() => this.setState({ specialColumnsOnly: !specialColumnsOnly })}
+            disabled={!transforms.length}
+          />
+        </div>
+        <ReactTable
+          data={transformQueryState.data.rows}
+          columns={filterMap(transformQueryState.data.header, (columnName, i) => {
+            if (!filterMatch(columnName, columnFilter)) return null;
+            const timestamp = columnName === '__time';
+            const transformIndex = transforms.findIndex(f => f.name === columnName);
+            if (transformIndex === -1 && specialColumnsOnly) return null;
+            const transform = transforms[transformIndex];
+
+            const columnClassName = classNames({
+              transformed: transform,
+              selected: transform && transformIndex === selectedTransformIndex
+            });
+            return {
+              Header: (
+                <div
+                  className={classNames('clickable')}
+                  onClick={() => {
+                    if (transform) {
+                      this.setState({
+                        selectedTransformIndex: transformIndex,
+                        selectedTransform: transform
+                      });
+                    } else {
+                      this.setState({
+                        selectedTransformIndex: -1,
+                        selectedTransform: {
+                          type: 'expression',
+                          name: columnName,
+                          expression: escapeColumnName(columnName)
+                        }
+                      });
+                    }
+                  }}
+                >
+                  <div className="column-name">{columnName}</div>
+                  <div className="column-detail">
+                    {transform ? `= ${transform.expression}` : ''}&nbsp;
+                  </div>
+                </div>
+              ),
+              headerClassName: columnClassName,
+              className: columnClassName,
+              id: String(i),
+              accessor: row => row.parsed ? row.parsed[columnName] : null,
+              Cell: row => <NullTableCell value={row.value} timestamp={timestamp}/>
+            };
+          })}
+          defaultPageSize={50}
+          showPagination={false}
+          sortable={false}
+          className="-striped -highlight"
+        />
+      </div>;
+    }
+
+    return <>
+      <div className="main">{mainFill}</div>
+      <div className="control">
+        <Callout className="intro">
+          <p className="optional">
+            Optional
+          </p>
+          <p>
+            Druid can perform simple <ExternalLink href="http://druid.io/docs/latest/ingestion/transform-spec.html#transforms">transforms</ExternalLink> of column values.
+          </p>
+          <p>
+            Click "Preview" to see the result of any specified transforms.
+          </p>
+        </Callout>
+        {this.renderTransformControls()}
+        <Button
+          text="Preview"
+          onClick={() => this.queryForTransform()}
+        />
+      </div>
+      {this.renderNextBar({
+        disabled: !transformQueryState.data,
+        onNextStage: () => {
+          if (!transformQueryState.data) return;
+          this.updateSpec(updateSchemaWithSample(spec, transformQueryState.data, 'specific', true));
+        }
+      })}
+    </>;
+  }
+
+  renderTransformControls() {
+    const { spec, selectedTransform, selectedTransformIndex } = this.state;
+
+    const close = () => {
+      this.setState({
+        selectedTransformIndex: -1,
+        selectedTransform: null
+      });
+    };
+
+    const closeAndQuery = () => {
+      close();
+      setTimeout(() => {
+        this.queryForTransform();
+      }, 10);
+    };
+
+    if (selectedTransform) {
+      return <div className="edit-controls">
+        <AutoForm
+          fields={getTransformFormFields()}
+          model={selectedTransform}
+          onChange={(selectedTransform) => this.setState({ selectedTransform })}
+        />
+        <div className="controls-buttons">
+          <Button
+            className="add-update"
+            text={selectedTransformIndex === -1 ? 'Add' : 'Update'}
+            intent={Intent.PRIMARY}
+            onClick={() => {
+              this.updateSpec(deepSet(spec, `dataSchema.transformSpec.transforms.${selectedTransformIndex}`, selectedTransform));
+              closeAndQuery();
+            }}
+          />
+          {
+            selectedTransformIndex !== -1 &&
+            <Button
+              icon={IconNames.TRASH}
+              intent={Intent.DANGER}
+              onClick={() => {
+                this.updateSpec(deepDelete(spec, `dataSchema.transformSpec.transforms.${selectedTransformIndex}`));
+                closeAndQuery();
+              }}
+            />
+          }
+          <Button className="cancel" text="Cancel" onClick={close}/>
+        </div>
+      </div>;
+
+    } else {
+      return <FormGroup>
+        <Button
+          text="Add column transform"
+          onClick={() => {
+            this.setState({
+              selectedTransformIndex: -1,
+              selectedTransform: { type: 'expression', name: '', expression: '' }
+            });
+          }}
+        />
+      </FormGroup>;
+    }
+  }
+
+  // ==================================================================
+
+  async queryForFilter(initRun = false) {
+    const { spec, cacheKey } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+
+    let issue: string | null = null;
+    if (issueWithIoConfig(ioConfig)) {
+      issue = `IoConfig not ready, ${issueWithIoConfig(ioConfig)}`;
+    } else if (issueWithParser(parser)) {
+      issue = `Parser not ready, ${issueWithParser(parser)}`;
+    }
+
+    if (issue) {
+      this.setState({
+        filterQueryState: initRun ? QueryState.INIT : new QueryState({ error: issue })
+      });
+      return;
+    }
+
+    this.setState({
+      filterQueryState: new QueryState({ loading: true })
+    });
+
+    let sampleResponse: SampleResponse;
+    try {
+      sampleResponse = await sampleForFilter(spec, cacheKey);
+    } catch (e) {
+      this.setState({
+        filterQueryState: new QueryState({ error: e.message })
+      });
+      return;
+    }
+
+    this.setState({
+      cacheKey: sampleResponse.cacheKey,
+      filterQueryState: new QueryState({
+        data: headerAndRowsFromSampleResponse(sampleResponse, undefined, true)
+      })
+    });
+  }
+
+  renderFilterStage() {
+    const { spec, columnFilter, filterQueryState, selectedFilter, selectedFilterIndex, showGlobalFilter } = this.state;
+    const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
+    const { dimensionFilters } = splitFilter(deepGet(spec, 'dataSchema.transformSpec.filter'));
+
+    const isBlank = !parseSpec.format;
+
+    let mainFill: JSX.Element | string = '';
+    if (filterQueryState.isInit()) {
+      mainFill = <CenterMessage>
+        Please enter more details for the previous steps
+      </CenterMessage>;
+
+    } else if (filterQueryState.isLoading()) {
+      mainFill = <Loader loading/>;
+
+    } else if (filterQueryState.error) {
+      mainFill = <CenterMessage>
+        {`Error: ${filterQueryState.error}`}
+      </CenterMessage>;
+
+    } else if (filterQueryState.data) {
+      mainFill = <div className="table-with-control">
+        <div className="table-control">
+          <ClearableInput
+            value={columnFilter}
+            onChange={(columnFilter) => this.setState({ columnFilter })}
+            placeholder="Search columns"
+          />
+        </div>
+        <ReactTable
+          data={filterQueryState.data.rows}
+          columns={filterMap(filterQueryState.data.header, (columnName, i) => {
+            if (!filterMatch(columnName, columnFilter)) return null;
+            const timestamp = columnName === '__time';
+            const filterIndex = dimensionFilters.findIndex(f => f.dimension === columnName);
+            const filter = dimensionFilters[filterIndex];
+
+            const columnClassName = classNames({
+              filtered: filter,
+              selected: filter && filterIndex === selectedFilterIndex
+            });
+            return {
+              Header: (
+                <div
+                  className={classNames('clickable')}
+                  onClick={() => {
+                    if (timestamp) {
+                      this.setState({
+                        showGlobalFilter: true
+                      });
+                    } else if (filter) {
+                      this.setState({
+                        selectedFilterIndex: filterIndex,
+                        selectedFilter: filter
+                      });
+                    } else {
+                      this.setState({
+                        selectedFilterIndex: -1,
+                        selectedFilter: { type: 'selector', dimension: columnName, value: '' }
+                      });
+                    }
+                  }}
+                >
+                  <div className="column-name">{columnName}</div>
+                  <div className="column-detail">
+                    {filter ? `(filtered)` : ''}&nbsp;
+                  </div>
+                </div>
+              ),
+              headerClassName: columnClassName,
+              className: columnClassName,
+              id: String(i),
+              accessor: row => row.parsed ? row.parsed[columnName] : null,
+              Cell: row => <NullTableCell value={row.value} timestamp={timestamp}/>
+            };
+          })}
+          defaultPageSize={50}
+          showPagination={false}
+          sortable={false}
+          className="-striped -highlight"
+        />
+      </div>;
+    }
+
+    return <>
+      <div className="main">{mainFill}</div>
+      <div className="control">
+        <Callout className="intro">
+          <p className="optional">
+            Optional
+          </p>
+          <p>
+            Druid can <ExternalLink href="http://druid.io/docs/latest/querying/filters.html">filter</ExternalLink> out unwanted data.
+          </p>
+          <p>
+            Click "Preview" to see the impact of any specified filters.
+          </p>
+        </Callout>
+        {!showGlobalFilter && this.renderColumnFilterControls()}
+        {!selectedFilter && this.renderGlobalFilterControls()}
+        {
+          (!selectedFilter && !showGlobalFilter) &&
+          <Button
+            text="Preview"
+            disabled={isBlank}
+            onClick={() => this.queryForFilter()}
+          />
+        }
+      </div>
+      {this.renderNextBar({})}
+    </>;
+  }
+
+  renderColumnFilterControls() {
+    const { spec, selectedFilter, selectedFilterIndex } = this.state;
+
+    const close = () => {
+      this.setState({
+        selectedFilterIndex: -1,
+        selectedFilter: null
+      });
+    };
+
+    const closeAndQuery = () => {
+      close();
+      setTimeout(() => {
+        this.queryForFilter();
+      }, 10);
+    };
+
+    if (selectedFilter) {
+      return <div className="edit-controls">
+        <AutoForm
+          fields={getFilterFormFields()}
+          model={selectedFilter}
+          onChange={(f) => this.setState({ selectedFilter: f })}
+        />
+        <div className="controls-buttons">
+          <Button
+            className="add-update"
+            text={selectedFilterIndex === -1 ? 'Add' : 'Update'}
+            intent={Intent.PRIMARY}
+            onClick={() => {
+              const curFilter = splitFilter(deepGet(spec, 'dataSchema.transformSpec.filter'));
+              const newFilter = joinFilter(deepSet(curFilter, `dimensionFilters.${selectedFilterIndex}`, selectedFilter));
+              this.updateSpec(deepSet(spec, 'dataSchema.transformSpec.filter', newFilter));
+              closeAndQuery();
+            }}
+          />
+          {
+            selectedFilterIndex !== -1 &&
+            <Button
+              icon={IconNames.TRASH}
+              intent={Intent.DANGER}
+              onClick={() => {
+                const curFilter = splitFilter(deepGet(spec, 'dataSchema.transformSpec.filter'));
+                const newFilter = joinFilter(deepDelete(curFilter, `dimensionFilters.${selectedFilterIndex}`));
+                this.updateSpec(deepSet(spec, 'dataSchema.transformSpec.filter', newFilter));
+                closeAndQuery();
+              }}
+            />
+          }
+          <Button className="cancel" text="Cancel" onClick={close}/>
+        </div>
+      </div>;
+    } else {
+      return <FormGroup>
+        <Button
+          text="Add column filter"
+          onClick={() => {
+            this.setState({
+              selectedFilter: { type: 'selector', dimension: '', value: '' },
+              selectedFilterIndex: -1
+            });
+          }}
+        />
+      </FormGroup>;
+    }
+  }
+
+  renderGlobalFilterControls() {
+    const { spec, showGlobalFilter } = this.state;
+    const intervals: string[] = deepGet(spec, 'dataSchema.granularitySpec.intervals');
+    const { restFilter } = splitFilter(deepGet(spec, 'dataSchema.transformSpec.filter'));
+    const hasGlobalFilter = Boolean(intervals || restFilter);
+
+    if (showGlobalFilter) {
+      return <div className="edit-controls">
+        <AutoForm
+          fields={[
+            {
+              name: 'dataSchema.granularitySpec.intervals',
+              label: 'Time intervals',
+              type: 'string-array',
+              placeholder: 'ex: 2018-01-01/2018-06-01',
+              info: <>
+                A comma separated list of intervals for the raw data being ingested.
+                Ignored for real-time ingestion.
+              </>
+            }
+          ]}
+          model={spec}
+          onChange={s => this.updateSpec(s)}
+        />
+        <FormGroup label="Extra filter">
+          <JSONInput
+            value={restFilter}
+            onChange={f => {
+              const curFilter = splitFilter(deepGet(spec, 'dataSchema.transformSpec.filter'));
+              const newFilter = joinFilter(deepSet(curFilter, `restFilter`, f));
+              this.updateSpec(deepSet(spec, 'dataSchema.transformSpec.filter', newFilter));
+            }}
+            height="200px"
+          />
+        </FormGroup>
+        <div className="controls-buttons">
+          <Button
+            className="add-update"
+            text="Preview"
+            intent={Intent.PRIMARY}
+            onClick={() => this.queryForFilter()}
+          />
+          <Button
+            className="cancel"
+            text="Close"
+            onClick={() => this.setState({ showGlobalFilter: false })}
+          />
+        </div>
+      </div>;
+    } else {
+      return <FormGroup>
+        <Button
+          text={`${hasGlobalFilter ? 'Edit' : 'Add'} global filter`}
+          onClick={() => this.setState({ showGlobalFilter: true })}
+        />
+      </FormGroup>;
+    }
+  }
+
+  // ==================================================================
+
+  async queryForSchema(initRun = false) {
+    const { spec, cacheKey } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
+
+    let issue: string | null = null;
+    if (issueWithIoConfig(ioConfig)) {
+      issue = `IoConfig not ready, ${issueWithIoConfig(ioConfig)}`;
+    } else if (issueWithParser(parser)) {
+      issue = `Parser not ready, ${issueWithParser(parser)}`;
+    }
+
+    if (issue) {
+      this.setState({
+        schemaQueryState: initRun ? QueryState.INIT : new QueryState({ error: issue })
+      });
+      return;
+    }
+
+    this.setState({
+      schemaQueryState: new QueryState({ loading: true })
+    });
+
+    let sampleResponse: SampleResponse;
+    try {
+      sampleResponse = await sampleForSchema(spec, cacheKey);
+    } catch (e) {
+      this.setState({
+        schemaQueryState: new QueryState({ error: e.message })
+      });
+      return;
+    }
+
+    this.setState({
+      cacheKey: sampleResponse.cacheKey,
+      schemaQueryState: new QueryState({
+        data: headerAndRowsFromSampleResponse(sampleResponse)
+      })
+    });
+  }
+
+  renderSchemaStage() {
+    const { spec, columnFilter, schemaQueryState, selectedDimensionSpec, selectedDimensionSpecIndex, selectedMetricSpec, selectedMetricSpecIndex } = this.state;
+    const metricsSpec: MetricSpec[] = deepGet(spec, 'dataSchema.metricsSpec') || [];
+    const dimensionsSpec: DimensionsSpec = deepGet(spec, 'dataSchema.parser.parseSpec.dimensionsSpec') || {};
+    const rollup: boolean = Boolean(deepGet(spec, 'dataSchema.granularitySpec.rollup'));
+    const somethingSelected = Boolean(selectedDimensionSpec || selectedMetricSpec);
+    const dimensionMode = getDimensionMode(spec);
+
+    let mainFill: JSX.Element | string = '';
+    if (schemaQueryState.isInit()) {
+      mainFill = <CenterMessage>
+        Please enter more details for the previous steps
+      </CenterMessage>;
+
+    } else if (schemaQueryState.isLoading()) {
+      mainFill = <Loader loading/>;
+
+    } else if (schemaQueryState.error) {
+      mainFill = <CenterMessage>
+        {`Error: ${schemaQueryState.error}`}
+      </CenterMessage>;
+
+    } else if (schemaQueryState.data) {
+      const dimensionMetricSortedHeader = sortWithPrefixSuffix(schemaQueryState.data.header, ['__time'], metricsSpec.map(getMetricSpecName));
+      mainFill = <div className="table-with-control">
+        <div className="table-control">
+          <ClearableInput
+            value={columnFilter}
+            onChange={(columnFilter) => this.setState({ columnFilter })}
+            placeholder="Search columns"
+          />
+        </div>
+        <ReactTable
+          data={schemaQueryState.data.rows}
+          columns={filterMap(dimensionMetricSortedHeader, (columnName, i) => {
+            if (!filterMatch(columnName, columnFilter)) return null;
+
+            const metricSpecIndex = metricsSpec.findIndex(m => getMetricSpecName(m) === columnName);
+            const metricSpec = metricsSpec[metricSpecIndex];
+
+            if (metricSpec) {
+              const columnClassName = classNames('metric', {
+                selected: metricSpec && metricSpecIndex === selectedMetricSpecIndex
+              });
+              return {
+                Header: (
+                  <div
+                    className="clickable"
+                    onClick={() => {
+                      this.setState({
+                        selectedMetricSpecIndex: metricSpecIndex,
+                        selectedMetricSpec: metricSpec,
+                        selectedDimensionSpecIndex: -1,
+                        selectedDimensionSpec: null
+                      });
+                    }}
+                  >
+                    <div className="column-name">{columnName}</div>
+                    <div className="column-detail">
+                      {metricSpec.type}&nbsp;
+                    </div>
+                  </div>
+                ),
+                headerClassName: columnClassName,
+                className: columnClassName,
+                id: String(i),
+                accessor: row => row.parsed ? row.parsed[columnName] : null,
+                Cell: row => <NullTableCell value={row.value}/>
+              };
+            } else {
+              const timestamp = columnName === '__time';
+              const dimensionSpecIndex = dimensionsSpec.dimensions ? dimensionsSpec.dimensions.findIndex(d => getDimensionSpecName(d) === columnName) : -1;
+              const dimensionSpec = dimensionsSpec.dimensions ? dimensionsSpec.dimensions[dimensionSpecIndex] : null;
+              const dimensionSpecType = dimensionSpec ? getDimensionSpecType(dimensionSpec) : null;
+
+              const columnClassName = classNames(timestamp ? 'timestamp' : 'dimension', dimensionSpecType || 'string', {
+                selected: dimensionSpec && dimensionSpecIndex === selectedDimensionSpecIndex
+              });
+              return {
+                Header: (
+                  <div
+                    className="clickable"
+                    onClick={() => {
+                      if (timestamp) {
+                        this.setState({
+                          selectedDimensionSpecIndex: -1,
+                          selectedDimensionSpec: null,
+                          selectedMetricSpecIndex: -1,
+                          selectedMetricSpec: null
+                        });
+                        return;
+                      }
+
+                      if (!dimensionSpec) return;
+                      this.setState({
+                        selectedDimensionSpecIndex: dimensionSpecIndex,
+                        selectedDimensionSpec: inflateDimensionSpec(dimensionSpec),
+                        selectedMetricSpecIndex: -1,
+                        selectedMetricSpec: null
+                      });
+                    }}
+                  >
+                    <div className="column-name">{columnName}</div>
+                    <div className="column-detail">
+                      {timestamp ? 'long (time column)' : (dimensionSpecType || 'string (auto)')}&nbsp;
+                    </div>
+                  </div>
+                ),
+                headerClassName: columnClassName,
+                className: columnClassName,
+                id: String(i),
+                accessor: (row: SampleEntry) => row.parsed ? row.parsed[columnName] : null,
+                Cell: row => <NullTableCell value={row.value} timestamp={timestamp}/>
+              };
+            }
+          })}
+          defaultPageSize={50}
+          showPagination={false}
+          sortable={false}
+          className="-striped -highlight"
+        />
+      </div>;
+    }
+
+    return <>
+      <div className="main">{mainFill}</div>
+      <div className="control">
+        <Callout className="intro">
+          <p>
+            Each column in Druid must have an assigned type (string, long, float, complex, etc).
+            Default primitive types have been automatically assigned to your columns.
+            If you want to change the type, click on the column header.
+          </p>
+          <p>
+            Select whether or not you want to <ExternalLink href="http://druid.io/docs/latest/tutorials/tutorial-rollup.html">roll-up</ExternalLink> your data.
+          </p>
+        </Callout>
+        {
+          !somethingSelected &&
+          <>
+            <FormGroup>
+              <Switch
+                checked={dimensionMode === 'specific'}
+                onChange={() => this.setState({ newDimensionMode: dimensionMode === 'specific' ? 'auto-detect' : 'specific' })}
+                label="Set dimensions and metrics"
+              />
+              <Popover
+                content={
+                  <div className="label-info-text">
+                    <p>
+                      Select whether or not you want to set an explicit list of <ExternalLink href="http://druid.io/docs/latest/ingestion/ingestion-spec.html#dimensionsspec">dimensions</ExternalLink> and <ExternalLink href="http://druid.io/docs/latest/querying/aggregations.html">metrics</ExternalLink>.
+                      Explicitly setting dimensions and metrics can lead to better compression and performance.
+                      If you disable this option, Druid will try to auto-detect fields in your data and treat them as individual columns.
+                    </p>
+                  </div>
+                }
+                position="left-bottom"
+              >
+                <Icon icon={IconNames.INFO_SIGN} iconSize={14}/>
+              </Popover>
+            </FormGroup>
+            {
+              dimensionMode === 'auto-detect' &&
+              <AutoForm
+                fields={[
+                  {
+                    name: 'dataSchema.parser.parseSpec.dimensionsSpec.dimensionExclusions',
+                    label: 'Exclusions',
+                    type: 'string-array',
+                    info: <>
+                      Provide a comma separated list of columns (use the column name from the raw data) you do not want Druid to ingest.
+                    </>
+                  }
+                ]}
+                model={spec}
+                onChange={s => this.updateSpec(s)}
+              />
+            }
+            <FormGroup>
+              <Switch
+                checked={rollup}
+                onChange={() => this.setState({ newRollup: !rollup })}
+                labelElement="Rollup"
+              />
+              <Popover
+                content={
+                  <div className="label-info-text">
+                    <p>
+                      If you enable roll-up, Druid will try to pre-aggregate data before indexing it to conserve storage.
+                      The primary timestamp will be truncated to the specified query granularity, and rows containing the same string field values will be aggregated together.
+                    </p>
+                    <p>
+                      If you enable rollup, you must specify which columns are <a href="http://druid.io/docs/latest/ingestion/ingestion-spec.html#dimensionsspec">dimensions</a> (fields you want to group and filter on), and which are <a href="http://druid.io/docs/latest/querying/aggregations.html">metrics</a> (fields you want to aggregate on).
+                    </p>
+                  </div>
+                }
+                position="left-bottom"
+              >
+                <Icon icon={IconNames.INFO_SIGN} iconSize={14}/>
+              </Popover>
+            </FormGroup>
+            <AutoForm
+              fields={[
+                {
+                  name: 'dataSchema.granularitySpec.queryGranularity',
+                  label: 'Query granularity',
+                  type: 'string',
+                  suggestions: ['NONE', 'MINUTE', 'HOUR', 'DAY'],
+                  info: <>
+                    This granularity determines how timestamps will be truncated (not at all, to the minute, hour, day, etc).
+                    After data is rolled up, this granularity becomes the minimum granularity you can query data at.
+                  </>
+                }
+              ]}
+              model={spec}
+              onChange={s => this.updateSpec(s)}
+            />
+          </>
+        }
+        {!selectedMetricSpec && this.renderDimensionSpecControls()}
+        {!selectedDimensionSpec && this.renderMetricSpecControls()}
+        {this.renderChangeRollupAction()}
+        {this.renderChangeDimensionModeAction()}
+      </div>
+      {this.renderNextBar({
+        disabled: !schemaQueryState.data
+      })}
+    </>;
+  }
+
+  renderChangeRollupAction() {
+    const { newRollup, spec, cacheKey } = this.state;
+    if (newRollup === null) return;
+
+    return <AsyncActionDialog
+      action={async () => {
+        const sampleResponse = await sampleForTransform(spec, cacheKey);
+        this.updateSpec(updateSchemaWithSample(spec, headerAndRowsFromSampleResponse(sampleResponse), getDimensionMode(spec), newRollup));
+        setTimeout(() => {
+          this.queryForSchema();
+        }, 10);
+      }}
+      confirmButtonText={`Yes - ${newRollup ? 'enable' : 'disable'} rollup`}
+      successText={`Rollup was ${newRollup ? 'enabled' : 'disabled'}. Schema has been updated.`}
+      failText="Could change rollup"
+      intent={Intent.WARNING}
+      onClose={() => this.setState({ newRollup: null })}
+    >
+      <p>
+        {`Are you sure you want to ${newRollup ? 'enable' : 'disable'} rollup?`}
+      </p>
+      <p>
+        Making this change will reset any work you have done in this section.
+      </p>
+    </AsyncActionDialog>;
+  }
+
+  renderChangeDimensionModeAction() {
+    const { newDimensionMode, spec, cacheKey } = this.state;
+    if (newDimensionMode === null) return;
+    const autoDetect = newDimensionMode === 'auto-detect';
+
+    return <AsyncActionDialog
+      action={async () => {
+        const sampleResponse = await sampleForTransform(spec, cacheKey);
+        this.updateSpec(updateSchemaWithSample(spec, headerAndRowsFromSampleResponse(sampleResponse), newDimensionMode, getRollup(spec)));
+        setTimeout(() => {
+          this.queryForSchema();
+        }, 10);
+      }}
+      confirmButtonText={`Yes - ${autoDetect ? 'auto detect' : 'explicitly set'} columns`}
+      successText={`Dimension mode changes to ${autoDetect ? 'auto detect' : 'specific list'}. Schema has been updated.`}
+      failText="Could change dimension mode"
+      intent={Intent.WARNING}
+      onClose={() => this.setState({ newDimensionMode: null })}
+    >
+      <p>
+        {
+          autoDetect ?
+          'Are you sure you don’t want to set the dimensions and metrics explicitly?' :
+          'Are you sure you want to set dimensions and metrics explicitly?'
+        }
+      </p>
+      <p>
+        Making this change will reset any work you have done in this section.
+      </p>
+    </AsyncActionDialog>;
+  }
+
+  renderDimensionSpecControls() {
+    const { spec, selectedDimensionSpec, selectedDimensionSpecIndex } = this.state;
+
+    const close = () => {
+      this.setState({
+        selectedDimensionSpecIndex: -1,
+        selectedDimensionSpec: null
+      });
+    };
+
+    const closeAndQuery = () => {
+      close();
+      setTimeout(() => {
+        this.queryForSchema();
+      }, 10);
+    };
+
+    if (selectedDimensionSpec) {
+      return <div className="edit-controls">
+        <AutoForm
+          fields={getDimensionSpecFormFields()}
+          model={selectedDimensionSpec}
+          onChange={(selectedDimensionSpec) => this.setState({ selectedDimensionSpec })}
+        />
+        <div className="controls-buttons">
+          <Button
+            className="add-update"
+            text={selectedDimensionSpecIndex === -1 ? 'Add' : 'Update'}
+            intent={Intent.PRIMARY}
+            onClick={() => {
+              this.updateSpec(deepSet(spec, `dataSchema.parser.parseSpec.dimensionsSpec.dimensions.${selectedDimensionSpecIndex}`, selectedDimensionSpec));
+              closeAndQuery();
+            }}
+          />
+          {
+            selectedDimensionSpecIndex !== -1 &&
+            <Button
+              icon={IconNames.TRASH}
+              intent={Intent.DANGER}
+              onClick={() => {
+                const curDimensions = deepGet(spec, `dataSchema.parser.parseSpec.dimensionsSpec.dimensions`) || [];
+                if (curDimensions.length <= 1) return; // Guard against removing the last dimension, ToDo: some better feedback here would be good
+
+                this.updateSpec(deepDelete(spec, `dataSchema.parser.parseSpec.dimensionsSpec.dimensions.${selectedDimensionSpecIndex}`));
+                closeAndQuery();
+              }}
+            />
+          }
+          <Button className="cancel" text="Cancel" onClick={close}/>
+        </div>
+      </div>;
+
+    } else {
+      return <FormGroup>
+        <Button
+          text="Add dimension"
+          disabled={getDimensionMode(spec) !== 'specific'}
+          onClick={() => {
+            this.setState({
+              selectedDimensionSpecIndex: -1,
+              selectedDimensionSpec: {
+                name: 'new_dimension',
+                type: 'string'
+              }
+            });
+          }}
+        />
+      </FormGroup>;
+    }
+  }
+
+  renderMetricSpecControls() {
+    const { spec, selectedMetricSpec, selectedMetricSpecIndex } = this.state;
+
+    const close = () => {
+      this.setState({
+        selectedMetricSpecIndex: -1,
+        selectedMetricSpec: null
+      });
+    };
+
+    const closeAndQuery = () => {
+      close();
+      setTimeout(() => {
+        this.queryForSchema();
+      }, 10);
+    };
+
+    if (selectedMetricSpec) {
+      return <div className="edit-controls">
+        <AutoForm
+          fields={getMetricSpecFormFields()}
+          model={selectedMetricSpec}
+          onChange={(selectedMetricSpec) => this.setState({ selectedMetricSpec })}
+        />
+        <div className="controls-buttons">
+          <Button
+            className="add-update"
+            text={selectedMetricSpecIndex === -1 ? 'Add' : 'Update'}
+            intent={Intent.PRIMARY}
+            onClick={() => {
+              this.updateSpec(deepSet(spec, `dataSchema.metricsSpec.${selectedMetricSpecIndex}`, selectedMetricSpec));
+              closeAndQuery();
+            }}
+          />
+          {
+            selectedMetricSpecIndex !== -1 &&
+            <Button
+              icon={IconNames.TRASH}
+              intent={Intent.DANGER}
+              onClick={() => {
+                this.updateSpec(deepDelete(spec, `dataSchema.metricsSpec.${selectedMetricSpecIndex}`));
+                closeAndQuery();
+              }}
+            />
+          }
+          <Button className="cancel" text="Cancel" onClick={close}/>
+        </div>
+      </div>;
+
+    } else {
+      return <FormGroup>
+        <Button
+          text="Add metric"
+          onClick={() => {
+            this.setState({
+              selectedMetricSpecIndex: -1,
+              selectedMetricSpec: {
+                name: 'sum_blah',
+                type: 'doubleSum',
+                fieldName: ''
+              }
+            });
+          }}
+        />
+      </FormGroup>;
+    }
+  }
+
+  // ==================================================================
+
+  renderPartitionStage() {
+    const { spec } = this.state;
+    const tuningConfig: TuningConfig = deepGet(spec, 'tuningConfig') || {};
+    const granularitySpec: GranularitySpec = deepGet(spec, 'dataSchema.granularitySpec') || {};
+    const myIsParallel = isParallel(spec);
+
+    return <>
+      <div className="main">
+        <H5>Primary partitioning (by time)</H5>
+        <AutoForm
+          fields={[
+            {
+              name: 'type',
+              type: 'string',
+              suggestions: ['uniform', 'arbitrary'],
+              info: <>
+                This spec is used to generated segments with uniform intervals.
+              </>
+            },
+            {
+              name: 'segmentGranularity',
+              type: 'string',
+              suggestions: ['HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'],
+              isDefined: (g: GranularitySpec) => g.type === 'uniform',
+              info: <>
+                The granularity to create time chunks at.
+                Multiple segments can be created per time chunk.
+                For example, with 'DAY' segmentGranularity, the events of the same day fall into the same time chunk which can be optionally further partitioned into multiple segments based on other configurations and input size.
+              </>
+            }
+          ]}
+          model={granularitySpec}
+          onChange={g => this.updateSpec(deepSet(spec, 'dataSchema.granularitySpec', g))}
+        />
+      </div>
+      <div className="other">
+        <H5>Secondary partitioning</H5>
+        <AutoForm
+          fields={[
+            {
+              name: 'partitionDimensions',
+              type: 'string-array',
+              disabled: myIsParallel,
+              info: <>
+                <p>
+                  Does not currently work with parallel ingestion
+                </p>
+                <p>
+                  The dimensions to partition on.
+                  Leave blank to select all dimensions. Only used with forceGuaranteedRollup = true, will be ignored otherwise.
+                </p>
+              </>
+            },
+            {
+              name: 'forceGuaranteedRollup',
+              type: 'boolean',
+              disabled: myIsParallel,
+              info: <>
+                <p>
+                  Does not currently work with parallel ingestion
+                </p>
+                <p>
+                  Forces guaranteeing the perfect rollup.
+                  The perfect rollup optimizes the total size of generated segments and querying time while indexing time will be increased.
+                  If this is set to true, the index task will read the entire input data twice: one for finding the optimal number of partitions per time chunk and one for generating segments.
+                </p>
+              </>
+            },
+            {
+              name: 'targetPartitionSize',
+              type: 'number',
+              info: <>
+                Target number of rows to include in a partition, should be a number that targets segments of 500MB~1GB.
+              </>
+            },
+            {
+              name: 'numShards',
+              type: 'number',
+              info: <>
+                Directly specify the number of shards to create.
+                If this is specified and 'intervals' is specified in the granularitySpec, the index task can skip the determine intervals/partitions pass through the data. numShards cannot be specified if maxRowsPerSegment is set.
+              </>
+            },
+            {
+              name: 'maxRowsPerSegment',
+              type: 'number',
+              defaultValue: 5000000,
+              info: <>
+                Determines how many rows are in each segment.
+              </>
+            },
+            {
+              name: 'maxTotalRows',
+              type: 'number',
+              defaultValue: 20000000,
+              info: <>
+                Total number of rows in segments waiting for being pushed.
+              </>
+            }
+          ]}
+          model={tuningConfig}
+          onChange={t => this.updateSpec(deepSet(spec, 'tuningConfig', t))}
+        />
+      </div>
+      <div className="control">
+        <Callout className="intro">
+          <p className="optional">
+            Optional
+          </p>
+          <p>
+            Configure how Druid will partition data.
+          </p>
+        </Callout>
+        {this.renderParallelPickerIfNeeded()}
+      </div>
+      {this.renderNextBar({})}
+    </>;
+  }
+
+  // ==================================================================
+
+  renderTuningStage() {
+    const { spec } = this.state;
+    const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
+    const tuningConfig: TuningConfig = deepGet(spec, 'tuningConfig') || {};
+
+    const ingestionComboType = getIngestionComboType(spec);
+    const inputTuningFields = ingestionComboType ? getIoConfigTuningFormFields(ingestionComboType) : null;
+    return <>
+      <div className="main">
+        <H5>Input tuning</H5>
+        {
+          inputTuningFields ?
+          (
+            inputTuningFields.length ?
+            <AutoForm
+              fields={inputTuningFields}
+              model={ioConfig}
+              onChange={c => this.updateSpec(deepSet(spec, 'ioConfig', c))}
+            /> :
+            <div>
+              {
+                ioConfig.firehose ?
+                  `No specific tuning configs for firehose of type '${deepGet(ioConfig, 'firehose.type')}'.` :
+                  `No specific tuning configs.`
+              }
+            </div>
+          ) :
+          <JSONInput
+            value={ioConfig}
+            onChange={c => this.updateSpec(deepSet(spec, 'ioConfig', c))}
+            height="300px"
+          />
+        }
+      </div>
+      <div className="other">
+        <H5>General tuning</H5>
+        <AutoForm
+          fields={getTuningSpecFormFields()}
+          model={tuningConfig}
+          onChange={t => this.updateSpec(deepSet(spec, 'tuningConfig', t))}
+        />
+      </div>
+      <div className="control">
+        <Callout className="intro">
+          <p className="optional">
+            Optional
+          </p>
+          <p>
+            Fine tune how Druid will ingest data.
+          </p>
+        </Callout>
+        {this.renderParallelPickerIfNeeded()}
+      </div>
+      {this.renderNextBar({})}
+    </>;
+  }
+
+  renderParallelPickerIfNeeded() {
+    const { spec } = this.state;
+    if (!hasParallelAbility(spec)) return null;
+
+    return <FormGroup>
+      <Switch
+        large
+        checked={isParallel(spec)}
+        onChange={() => this.updateSpec(changeParallel(spec, !isParallel(spec)))}
+        labelElement={<>
+          {'Parallel indexing '}
+          <Popover
+            content={
+              <div className="label-info-text">
+                Druid currently has two types of native batch indexing tasks, <Code>index_parallel</Code> which runs tasks in parallel on multiple MiddleManager processes, and <Code>index</Code> which will run a single indexing task locally on a single MiddleManager.
+              </div>
+            }
+            position="left-bottom"
+          >
+            <Icon icon={IconNames.INFO_SIGN} iconSize={16}/>
+          </Popover>
+        </>}
+      />
+    </FormGroup>;
+  }
+
+  // ==================================================================
+
+  renderPublishStage() {
+    const { spec } = this.state;
+
+    return <>
+      <div className="main">
+        <H5>Publish configuration</H5>
+        <AutoForm
+          fields={[
+            {
+              name: 'dataSchema.dataSource',
+              label: 'Datasource name',
+              type: 'string',
+              info: <>
+                This is the name of the data source (table) in Druid.
+              </>
+            },
+            {
+              name: 'ioConfig.appendToExisting',
+              label: 'Append to existing',
+              type: 'boolean',
+              info: <>
+                Creates segments as additional shards of the latest version, effectively appending to the segment set instead of replacing it.
+              </>
+            }
+          ]}
+          model={spec}
+          onChange={s => this.updateSpec(s)}
+        />
+      </div>
+      <div className="other"/>
+      <div className="control">
+        <Callout className="intro">
+          <p>
+            Configure behavior of indexed data once it reaches Druid.
+          </p>
+        </Callout>
+      </div>
+      {this.renderNextBar({})}
+    </>;
+  }
+
+  // ==================================================================
+
+  renderJsonSpecStage() {
+    const { goToTask } = this.props;
+    const { spec } = this.state;
+
+    return <>
+      <div className="main">
+        <JSONInput
+          value={spec}
+          onChange={(s) => {
+            if (!s) return;
+            this.updateSpec(s);
+          }}
+          height="100%"
+        />
+      </div>
+      <div className="control">
+        <Callout className="intro">
+          <p className="optional">
+            Optional
+          </p>
+          <p>
+            Druid begins ingesting data once you submit a JSON ingestion spec.
+            If you modify any values in this view, the values entered in previous sections will update accordingly.
+            If you modify any values in previous sections, this spec will automatically update.
+          </p>
+          <p>
+            Submit the spec to begin loading data into Druid.
+          </p>
+        </Callout>
+      </div>
+      <div className="next-bar">
+        <Button
+          text="Submit"
+          intent={Intent.PRIMARY}
+          onClick={async () => {
+            if (['index', 'index_parallel'].includes(deepGet(spec, 'type'))) {
+              let taskResp: any;
+              try {
+                taskResp = await axios.post('/druid/indexer/v1/task', {
+                  type: spec.type,
+                  spec
+                });
+              } catch (e) {
+                AppToaster.show({
+                  message: `Failed to submit task: ${getDruidErrorMessage(e)}`,
+                  intent: Intent.DANGER
+                });
+                return;
+              }
+
+              AppToaster.show({
+                message: 'Task submitted successfully. Going to task view...',
+                intent: Intent.SUCCESS
+              });
+
+              setTimeout(() => {
+                goToTask(taskResp.data.task);
+              }, 1000);
+
+            } else {
+              try {
+                await axios.post('/druid/indexer/v1/supervisor', spec);
+              } catch (e) {
+                AppToaster.show({
+                  message: `Failed to submit supervisor: ${getDruidErrorMessage(e)}`,
+                  intent: Intent.DANGER
+                });
+                return;
+              }
+
+              AppToaster.show({
+                message: 'Supervisor submitted successfully. Going to task view...',
+                intent: Intent.SUCCESS
+              });
+
+              setTimeout(() => {
+                goToTask(null);
+              }, 1000);
+
+            }
+          }}
+        />
+      </div>
+    </>;
+  }
+
+}
diff --git a/web-console/src/views/sql-view.scss b/web-console/src/views/sql-view.scss
index 1cd7ecc..2cc8876 100644
--- a/web-console/src/views/sql-view.scss
+++ b/web-console/src/views/sql-view.scss
@@ -36,10 +36,6 @@
 
   .ReactTable {
     flex: 1;
-
-    .null-table-cell {
-      font-style: italic;
-    }
   }
 }
 
diff --git a/web-console/src/views/sql-view.tsx b/web-console/src/views/sql-view.tsx
index 9ae57d3..bdd297c 100644
--- a/web-console/src/views/sql-view.tsx
+++ b/web-console/src/views/sql-view.tsx
@@ -20,6 +20,7 @@ import * as Hjson from 'hjson';
 import * as React from 'react';
 import ReactTable from 'react-table';
 
+import { NullTableCell } from '../components/null-table-cell';
 import { SqlControl } from '../components/sql-control';
 import { QueryPlanDialog } from '../dialogs/query-plan-dialog';
 import {
@@ -200,11 +201,7 @@ export class SqlView extends React.Component<SqlViewProps, SqlViewState> {
           return {
             Header: h,
             accessor: String(i),
-            Cell: row => {
-              const value = row.value;
-              if (value === '' || value === null) return <span className="null-table-cell">null</span>;
-              return value;
-            }
+            Cell: row => <NullTableCell value={row.value}/>
           };
         })
       }
diff --git a/web-console/src/views/tasks-view.tsx b/web-console/src/views/tasks-view.tsx
index c179d49..b5a412a 100644
--- a/web-console/src/views/tasks-view.tsx
+++ b/web-console/src/views/tasks-view.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Alert, Button, ButtonGroup, Intent, Label } from '@blueprintjs/core';
+import { Alert, Button, ButtonGroup, Intent, Label, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import * as React from 'react';
@@ -34,10 +34,13 @@ import {
   booleanCustomTableFilter,
   countBy,
   formatDuration,
-  getDruidErrorMessage, LocalStorageKeys,
+  getDruidErrorMessage, localStorageGet, LocalStorageKeys,
   queryDruidSql,
   QueryManager, TableColumnSelectionHandler
 } from '../utils';
+import { IngestionType } from '../utils/ingestion-spec';
+
+import { LoadDataViewSeed } from './load-data-view';
 
 import './tasks-view.scss';
 
@@ -48,6 +51,7 @@ export interface TasksViewProps extends React.Props<any> {
   taskId: string | null;
   goToSql: (initSql: string) => void;
   goToMiddleManager: (middleManager: string) => void;
+  goToLoadDataView: () => void;
   noSqlMode: boolean;
 }
 
@@ -71,6 +75,7 @@ export interface TasksViewState {
 
   supervisorSpecDialogOpen: boolean;
   taskSpecDialogOpen: boolean;
+  initSpec: any;
   alertErrorMsg: string | null;
 }
 
@@ -131,6 +136,7 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
 
       supervisorSpecDialogOpen: false,
       taskSpecDialogOpen: false,
+      initSpec: null,
       alertErrorMsg: null
 
     };
@@ -224,6 +230,14 @@ ORDER BY "rank" DESC, "created_time" DESC`);
     this.taskQueryManager.terminate();
   }
 
+  private closeSpecDialogs = () => {
+    this.setState({
+      supervisorSpecDialogOpen: false,
+      taskSpecDialogOpen: false,
+      initSpec: null
+    });
+  }
+
   private submitSupervisor = async (spec: JSON) => {
     try {
       await axios.post('/druid/indexer/v1/supervisor', spec);
@@ -447,7 +461,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
             show: supervisorTableColumnSelectionHandler.showColumn('Actions')
           }
         ]}
-        defaultPageSize={10}
+        defaultPageSize={5}
         className="-striped -highlight"
       />
       {this.renderResumeSupervisorAction()}
@@ -614,10 +628,21 @@ ORDER BY "rank" DESC, "created_time" DESC`);
   }
 
   render() {
-    const { goToSql, noSqlMode } = this.props;
-    const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg } = this.state;
+    const { goToSql, goToLoadDataView, noSqlMode } = this.props;
+    const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, initSpec, alertErrorMsg } = this.state;
     const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
 
+    const submitTaskMenu = <Menu>
+      <MenuItem
+        text="Raw JSON task"
+        onClick={() => this.setState({ taskSpecDialogOpen: true })}
+      />
+      <MenuItem
+        text="Go to data loader"
+        onClick={() => goToLoadDataView()}
+      />
+    </Menu>;
+
     return <div className="tasks-view app-view">
       <ViewControlBar label="Supervisors">
         <Button
@@ -661,11 +686,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
             onClick={() => goToSql(this.taskQueryManager.getLastQuery())}
           />
         }
-        <Button
-          icon={IconNames.PLUS}
-          text="Submit task"
-          onClick={() => this.setState({ taskSpecDialogOpen: true })}
-        />
+        <Popover content={submitTaskMenu} position={Position.BOTTOM_LEFT}>
+          <Button icon={IconNames.PLUS} text="Submit task"/>
+        </Popover>
         <TableColumnSelection
           columns={taskTableColumns}
           onChange={(column) => taskTableColumnSelectionHandler.changeTableColumnSelection(column)}
@@ -673,16 +696,24 @@ ORDER BY "rank" DESC, "created_time" DESC`);
         />
       </ViewControlBar>
       {this.renderTaskTable()}
-      { supervisorSpecDialogOpen ? <SpecDialog
-        onClose={() => this.setState({ supervisorSpecDialogOpen: false })}
-        onSubmit={this.submitSupervisor}
-        title="Submit supervisor"
-      /> : null }
-      { taskSpecDialogOpen ? <SpecDialog
-        onClose={() => this.setState({ taskSpecDialogOpen: false })}
-        onSubmit={this.submitTask}
-        title="Submit task"
-      /> : null }
+      {
+        supervisorSpecDialogOpen &&
+        <SpecDialog
+          onClose={this.closeSpecDialogs}
+          onSubmit={this.submitSupervisor}
+          title="Submit supervisor"
+          initSpec={initSpec}
+        />
+      }
+      {
+        taskSpecDialogOpen &&
+        <SpecDialog
+          onClose={this.closeSpecDialogs}
+          onSubmit={this.submitTask}
+          title="Submit task"
+          initSpec={initSpec}
+        />
+      }
       <Alert
         icon={IconNames.ERROR}
         intent={Intent.PRIMARY}
diff --git a/web-console/tsconfig.json b/web-console/tsconfig.json
index a445cfa..6c1b4c9 100644
--- a/web-console/tsconfig.json
+++ b/web-console/tsconfig.json
@@ -26,6 +26,7 @@
     "lib/sql-function-doc.ts"
   ],
   "exclude": [
-    "**/*.test.ts"
+    "**/*.spec.ts",
+    "**/*.spec.tsx"
   ]
 }
diff --git a/web-console/webpack.config.js b/web-console/webpack.config.js
index cc248a2..ecbce77 100644
--- a/web-console/webpack.config.js
+++ b/web-console/webpack.config.js
@@ -19,11 +19,20 @@
 const process = require('process');
 const path = require('path');
 const postcssPresetEnv = require('postcss-preset-env');
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 const { version } = require('./package.json');
 
 module.exports = (env) => {
-  const druidUrl = 'http://' + ((env || {}).druid_host || process.env.druid_host || 'localhost:8888');
+  let druidUrl = ((env || {}).druid_host || process.env.druid_host || 'localhost');
+  if (!druidUrl.startsWith('http')) druidUrl = 'http://' + druidUrl;
+  if (!/:\d+$/.test(druidUrl)) druidUrl += ':8888';
+
+  const proxyTarget = {
+    target: druidUrl,
+    secure: false
+  };
+
   return {
     mode: process.env.NODE_ENV || 'development',
     entry: {
@@ -42,10 +51,11 @@ module.exports = (env) => {
     devServer: {
       publicPath: '/public',
       index: './index.html',
+      openPage: 'unified-console.html',
       port: 18081,
       proxy: {
-        '/status': druidUrl,
-        '/druid': druidUrl
+        '/status': proxyTarget,
+        '/druid': proxyTarget
       }
     },
     module: {
@@ -89,6 +99,9 @@ module.exports = (env) => {
           ]
         }
       ]
-    }
+    },
+    plugins: [
+      // new BundleAnalyzerPlugin()
+    ]
   };
 };


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