diff --git a/.eslintrc b/.eslintrc
index f522a534d6aba867c232b5e9876a9b7e0583b416..9dbe198988b824953d24c71806c75c80a096765f 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,7 +2,7 @@
     "rules": {
         "strict": [2, "never"],
         "no-undef": 2,
-        "no-unused-vars": [1, {"vars": "all", "args": "none", "varsIgnorePattern": "^(React|PropTypes|Component)$"}],
+        "no-unused-vars": [1, {"vars": "all", "args": "none"}],
         "import/no-commonjs": 1,
         "quotes": 0,
         "camelcase": 0,
diff --git a/OSX/Metabase/Backend/TaskHealthChecker.m b/OSX/Metabase/Backend/TaskHealthChecker.m
index 6a4b9d8a3009dcfc0aaf4b2708d99b3dc7436beb..cfb2221e83b6598f917674465d80161801dd8822 100644
--- a/OSX/Metabase/Backend/TaskHealthChecker.m
+++ b/OSX/Metabase/Backend/TaskHealthChecker.m
@@ -9,10 +9,10 @@
 #import "TaskHealthChecker.h"
 
 /// Check out health every this many seconds
-static const CGFloat HealthCheckIntervalSeconds = 1.2f;
+static const CGFloat HealthCheckIntervalSeconds = 2.0f;
 
 /// This number should be lower than HealthCheckIntervalSeconds so requests don't end up piling up
-static const CGFloat HealthCheckRequestTimeout = 0.25f;
+static const CGFloat HealthCheckRequestTimeout = 1.75f;
 
 /// After this many seconds of being unhealthy, consider the task timed out so it can be killed
 static const CFTimeInterval TimeoutIntervalSeconds = 60.0f;
@@ -50,7 +50,7 @@ static const CFTimeInterval TimeoutIntervalSeconds = 60.0f;
 	
 	[self resetTimeout];
 	
-	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
 		self.healthCheckTimer = [NSTimer timerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES];
 		self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f;
 		[[NSRunLoop mainRunLoop] addTimer:self.healthCheckTimer forMode:NSRunLoopCommonModes];
@@ -91,8 +91,9 @@ static const CFTimeInterval TimeoutIntervalSeconds = 60.0f;
 }
 
 - (void)checkHealth {
+	// run the health check on the high-priorty GCD queue because it's imperative that it complete so we can get an accurate picture of Mac App health
 	__weak TaskHealthChecker *weakSelf = self;
-	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
 		[weakSelf checkHealth:^(BOOL healthy) {
 			if (!healthy) NSLog(@"😷");
 			if (healthy && !weakSelf.healthy) NSLog(@"✅");
diff --git a/README.md b/README.md
index 6c3dff4579d816e6ca3c12071988c2bd5f01c904..98437a7abbf4fa5609eaa0552d874e3acbd85d7e 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ For more information check out [metabase.com](http://www.metabase.com)
 - CrateDB
 - Oracle
 - Vertica
+- Presto
 
 Don't see your favorite database? File an issue to let us know.
 
diff --git a/bin/ci b/bin/ci
index 461fdffa284967ba745f08edadf10ba88f525c38..4869c1cd4ae31e520fe065a3278870a386de2183 100755
--- a/bin/ci
+++ b/bin/ci
@@ -19,11 +19,15 @@ node-1() {
         run_step lein-test
 }
 node-2() {
-    is_enabled "drivers" && export ENGINES="h2,postgres,sqlite" || export ENGINES="h2"
+    is_enabled "drivers" && export ENGINES="h2,postgres,sqlite,presto" || export ENGINES="h2"
     if is_engine_enabled "crate"; then
         run_step install-crate
     fi
+    if is_engine_enabled "presto"; then
+        run_step install-presto
+    fi
     MB_ENCRYPTION_SECRET_KEY='Orw0AAyzkO/kPTLJRxiyKoBHXa/d6ZcO+p+gpZO/wSQ=' MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost \
+        MB_PRESTO_HOST=localhost MB_PRESTO_PORT=8080 \
         run_step lein-test
 }
 node-3() {
@@ -91,6 +95,11 @@ install-vertica() {
     sleep 60
 }
 
+install-presto() {
+    docker run --detach --publish 8080:8080 wiill/presto-mb-ci
+    sleep 10
+}
+
 lein-test() {
     lein test
 }
diff --git a/docs/administration-guide/databases/cratedb.md b/docs/administration-guide/databases/cratedb.md
index dcc6f5da89561b51876db3810b8a325069ad55d1..10864759412c4e9b516080acb1db35ec1e27278a 100644
--- a/docs/administration-guide/databases/cratedb.md
+++ b/docs/administration-guide/databases/cratedb.md
@@ -15,4 +15,8 @@ Starting in v0.18.0 Metabase provides a driver for connecting to CrateDB directl
 
 3. Click the `Save` button. Done.
 
-Metabase will now begin inspecting your CrateDB Dataset and finding any tables and fields to build up a sense for the schema. Give it a little bit of time to do its work and then you're all set to start querying.
\ No newline at end of file
+Metabase will now begin inspecting your CrateDB Dataset and finding any tables and fields to build up a sense for the schema. Give it a little bit of time to do its work and then you're all set to start querying.
+
+### Known limitations
+
+* Columns/Fields of type `object_array` are deactivated and not exposed. However, their nested fields are listed and also supported for queries.
\ No newline at end of file
diff --git a/docs/api-documentation.md b/docs/api-documentation.md
index 53bd42dfedfb3aecab2292023250be8315415ad3..72bf4b3df84005d9816d8b543b1d751d18f28eec 100644
--- a/docs/api-documentation.md
+++ b/docs/api-documentation.md
@@ -1,4 +1,4 @@
-# API Documentation for Metabase v0.23.0-snapshot
+# API Documentation for Metabase v0.24.0-snapshot
 
 ## `GET /api/activity/`
 
@@ -150,6 +150,8 @@ Run the query associated with a Card.
 
 *  **`parameters`** 
 
+*  **`ignore_cache`** value may be nil, or if non-nil, value must be a boolean.
+
 
 ## `POST /api/card/:card-id/query/csv`
 
@@ -193,7 +195,7 @@ Update a `Card`.
 
 *  **`visualization_settings`** value may be nil, or if non-nil, value must be a map.
 
-*  **`description`** value may be nil, or if non-nil, value must be a non-blank string.
+*  **`description`** value may be nil, or if non-nil, value must be a string.
 
 *  **`archived`** value may be nil, or if non-nil, value must be a boolean.
 
@@ -420,7 +422,7 @@ Update a `Dashboard`.
 
 *  **`points_of_interest`** value may be nil, or if non-nil, value must be a non-blank string.
 
-*  **`description`** value may be nil, or if non-nil, value must be a non-blank string.
+*  **`description`** value may be nil, or if non-nil, value must be a string.
 
 *  **`show_in_getting_started`** value may be nil, or if non-nil, value must be a non-blank string.
 
@@ -587,7 +589,7 @@ You must be a superuser to do this.
 
 ## `POST /api/dataset/`
 
-Execute an MQL query and retrieve the results as JSON.
+Execute a query and retrieve the results in the usual format.
 
 ##### PARAMS:
 
@@ -1286,6 +1288,8 @@ Create a new `Pulse`.
 
 *  **`channels`** value must be an array. Each value must be a map. The array cannot be empty.
 
+*  **`skip_if_empty`** value must be a boolean.
+
 
 ## `POST /api/pulse/test`
 
@@ -1299,6 +1303,8 @@ Test send an unsaved pulse.
 
 *  **`channels`** value must be an array. Each value must be a map. The array cannot be empty.
 
+*  **`skip_if_empty`** value must be a boolean.
+
 
 ## `PUT /api/pulse/:id`
 
@@ -1314,6 +1320,8 @@ Update a `Pulse` with ID.
 
 *  **`channels`** value must be an array. Each value must be a map. The array cannot be empty.
 
+*  **`skip_if_empty`** value must be a boolean.
+
 
 ## `GET /api/revision/`
 
@@ -1482,8 +1490,6 @@ Send a reset email when user has forgotten their password.
 
 *  **`remote-address`** 
 
-*  **`request`** 
-
 
 ## `POST /api/session/google_auth`
 
@@ -1561,8 +1567,6 @@ Special endpoint for creating the first user during setup.
 
 *  **`first_name`** value must be a non-blank string.
 
-*  **`request`** 
-
 *  **`password`** Insufficient password strength
 
 *  **`name`** 
@@ -1786,7 +1790,7 @@ Update a user's password.
 
 ## `PUT /api/user/:id/qbnewb`
 
-Indicate that a user has been informed about the vast intricacies of 'the' QueryBuilder.
+Indicate that a user has been informed about the vast intricacies of 'the' Query Builder.
 
 ##### PARAMS:
 
diff --git a/docs/operations-guide/start.md b/docs/operations-guide/start.md
index e405a26d087690332e9ea1fb216a4df1467c929a..ab59cd165887477e196a5525b880e944bfdedad3 100644
--- a/docs/operations-guide/start.md
+++ b/docs/operations-guide/start.md
@@ -98,6 +98,21 @@ If this happens, setting a few JVM options should fix your issue:
 
 Alternatively, you can upgrade to Java 8 instead, which will fix the issue as well.
 
+### Metabase fails to connect to H2 Database on Windows 10
+
+In some situations the Metabase JAR needs to be unblocked so it has permissions to create local files for the application database.
+
+On Windows 10, if you see an error message like
+
+    Exception in thread "main" java.lang.AssertionError: Assert failed: Unable to connect to Metabase DB.
+
+when running the JAR, you can unblock the file by right-clicking, clicking "Properties", and then clicking "Unblock".
+See Microsoft's documentation [here](https://blogs.msdn.microsoft.com/delay/p/unblockingdownloadedfile/) for more details on unblocking downloaded files.
+
+There are a few other reasons why Metabase might not be able to connect to your H2 DB. Metabase connects to the DB over a TCP port, and it's possible
+that something in your `ipconfig` configuration is blocking the H2 port. See the discussion [here](https://github.com/metabase/metabase/issues/1871) for
+details on how to resolve this issue.
+
 
 # Configuring the Metabase Application Database
 
diff --git a/frontend/src/metabase/App.jsx b/frontend/src/metabase/App.jsx
index 73954a11290ef435c4a124c800d21a31bea3a1b0..89922c39f14769a232e61c36a5a64bdbe913df61 100644
--- a/frontend/src/metabase/App.jsx
+++ b/frontend/src/metabase/App.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import Navbar from "metabase/nav/containers/Navbar.jsx";
diff --git a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
index 29c31dbd99275f2afc8c53df334af36ec1f78098..7db4fd4a3abfc7c97c6313860c715d6893a1af5c 100644
--- a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
+++ b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
@@ -1,8 +1,11 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
+import * as Urls from "metabase/lib/urls";
+
 export default class CreatedDatabaseModal extends Component {
     static propTypes = {
         databaseId: PropTypes.number.isRequired,
@@ -22,7 +25,7 @@ export default class CreatedDatabaseModal extends Component {
                         We're analyzing its schema now to make some educated guesses about its
                         metadata. <Link to={"/admin/datamodel/database/"+databaseId}>View this
                         database</Link> in the Data Model section to see what we've found and to
-                        make edits, or <Link to={"/q#?db="+databaseId}>ask a question</Link> about
+                        make edits, or <Link to={Urls.question(null, `?db=${databaseId}`)}>ask a question</Link> about
                         this database.
                     </p>
                 </div>
diff --git a/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx b/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx
index feeb38e6dbe951e6ad8a590180343e25e7972e56..ffdcb6bcdf7364f73406a100946a65240559d15d 100644
--- a/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx
+++ b/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import DatabaseDetailsForm from "metabase/components/DatabaseDetailsForm.jsx";
diff --git a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
index 920c65e77fabeef9271b229c54644441f55002f4..1498d90312e97eaae8acec2407b2ca8b070dd528 100644
--- a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
+++ b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
index ad8c03784e8bf4f337c21d653f247f5281ece429..b3e122cbc78e59a1dedd976dc1c45c2e8ab5cbe2 100644
--- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import MetabaseSettings from "metabase/lib/settings";
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx
index 439a2c7246ee42152036e345a49f37340dbdea80..ab7241838a81395ecb91c0bcdb54cf1e90b276fd 100644
--- a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { Link } from "react-router";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/FormInput.jsx b/frontend/src/metabase/admin/datamodel/components/FormInput.jsx
index 0cb6812714174fec17835e381f7893e0079f50a6..bfce1383e6f27116d91c5ada25e3af8a1d47a4a6 100644
--- a/frontend/src/metabase/admin/datamodel/components/FormInput.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FormInput.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import cx from "classnames";
 import { formDomOnlyProps } from "metabase/lib/redux";
diff --git a/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx b/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx
index de2e6fdb440de7938ae07b3e6a4e168bad06de63..c45ee4b1208e5601dab8a96356e20eed93ca81c7 100644
--- a/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 export default class FormLabel extends Component {
     static propTypes = {
diff --git a/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx b/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx
index e440768bc3e53ce118678d438daba3ba15b9d6b2..a2119223ab145de400721a0a35d488909cbcebfa 100644
--- a/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx
index 9910e1a11a7a4915d4884a6c09ba60046ec5da5c..7e03f702055f9f6ba417f7be4007b864a4baa319 100644
--- a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx
index 4393e9d0b1621ed908e63510ceebb3926c89cc85..7f4b8b32638d6d23f88cd47b689e2cc9fdaaab5d 100644
--- a/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
index 133fc5c27f05bcdf1b76aa478e0e8056e9cb8067..ac46b9c868c425a7e745338ba50f53746b3221bc 100644
--- a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
@@ -1,8 +1,9 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import GuiQueryEditor from "metabase/query_builder/components/GuiQueryEditor.jsx";
 
-import { serializeCardForUrl } from "metabase/lib/card";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 
@@ -56,7 +57,7 @@ export default class PartialQueryBuilder extends Component {
                 }
             }
         };
-        let previewUrl = "/q#" + serializeCardForUrl(previewCard);
+        let previewUrl = Urls.question(null, previewCard);
 
         const onChange = (query) => {
             this.props.onChange(query);
diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
index 752e30bdca0a33e3813c6978d6653a1e32e4a9a9..a1283fd11080ced28ad6dc43f862172320ec0e77 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Input from "metabase/components/Input.jsx";
 import Select from "metabase/components/Select.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx
index 7fce80642ffc7c0fd4a59755bd00d33cf3639053..306b46a5cc5c22347e90394618fa397a8f21c9ae 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ColumnItem from "./ColumnItem.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx
index 7ce8cf02a275d396c60dea167b65759e7bbaad2a..24900337e631605128b91d715b474b9a869bd6ce 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import SaveStatus from "metabase/components/SaveStatus.jsx";
 import Toggle from "metabase/components/Toggle.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx
index 55c4a4e88ff43d029ebafe510823b3c9c30cfbd5..571cbaed564a98a8d413e0a65ff2095ea089d240 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 export default class MetadataSchema extends Component {
     static propTypes = {
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx
index ef5ecba11ba5a4f7f0ef223e6aa8b710388862c2..f5a1220ef3aaa7d3afa8d9e68c0452366d6e1f03 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx
index 92b1a392780814fca12e6ab9b74e10cc2e44c702..190f29442097dabc5623bc672a3d99a65301b12f 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetricsList from "./MetricsList.jsx";
 import ColumnsList from "./ColumnsList.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx
index eeea47c7c4d2e306e66512813119b715030bcc67..5c0897a543e8d4deae1f443b8c71862f07880d68 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ProgressBar from "metabase/components/ProgressBar.jsx";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx
index fd9a65a18250cc0e264b1407b435dd24620bb3c9..6c6df516e693a1195e1280cfd8d562821eb2c456 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetadataTableList from "./MetadataTableList.jsx";
 import MetadataSchemaList from "./MetadataSchemaList.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx
index c3149b62a0b07039cc9835d003d4fbfc64dfdcdf..fa8ce0053fcab6d515d178cad42bd285d34eb43e 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ObjectActionSelect from "../ObjectActionSelect.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx
index 15197ce4b1abe022cfb6aae92163e672bf90f908..b2b0452db84b39e864ac32be2baee3615a13893e 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import MetricItem from "./MetricItem.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx
index 85afc4e7c26d9d073748bc3f5c2228bfb0b0377c..529d3b5e9377d526ab7e348cd3abfb913dc02e0e 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ObjectActionSelect from "../ObjectActionSelect.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx
index f6c7e032a3969a49d7a12739ab4704aa09a651a6..4035b6bd5fac66e54c689f27434adb15dace9ecc 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import SegmentItem from "./SegmentItem.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx
index fecf6b6ff1530875af886e2af55268b138402503..9d6e29ef3e947177df5ec253b4604fa7e0543568 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx
index 9a2e79460fa089607ff78faae67cb34199bbd578..7a5149b28ac10d51ccc14854ace086261c4df028 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import RevisionDiff from "./RevisionDiff.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx
index 6d0a0fef8da69e575c7f78b3469828da23d333c8..410bab3eab61013192e072fc21890bc8848f3813 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import TextDiff from "./TextDiff.jsx";
 import QueryDiff from "./QueryDiff.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx
index 08dafb3bd6a231978af2cf9b2ad5ebe29e7464ea..69535b8e52765be0b97c45660f4904a62cb167bc 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Revision from "./Revision.jsx";
 
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx
index 922a0ad0fe7b372bc4cf18158b6f2099d9251c80..507eb67c54bbfc3c2a8ea3f390fd845cc16a5d3a 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx
@@ -1,6 +1,7 @@
 /*eslint-env node */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import { diffWords } from 'diff';
 
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
index 5c96a1335d54fbfaa1dfaa3dce172d5a71d5194f..cfba3ebbf3a95950eed261532a2437ad8cab0914 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import _ from "underscore";
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
index a8f4cf5ad2230b39a140eb214cda0dc81e5df19a..19707a34cde6dcc1dc5957c6b4377cae1a0624a8 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
index 5ad0b08922d20b80bc018b316d59c9e614e54d6e..2029c47fa6eb6cbc00556513d591da5a5c6db990 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 
 import FormLabel from "../components/FormLabel.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx b/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx
index d5e2f1d2aeb00498fca0f6c29a01117ad0b92b09..bf4043b2a553838b40ebf49b8466f27d64e5664f 100644
--- a/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import RevisionHistory from "../components/revisions/RevisionHistory.jsx";
diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
index 735eca3e2d7a1b8dca6893ae483e8f74c1f49a1f..a4fbba0163f443aa6640a30aa9fa8c4edfc1d1f6 100644
--- a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
index cd99d32ff2475fa2f0fc75353fed4ee827892ea7..9f5bf96c06559e8241ebdc6d39d9284aeed11e54 100644
--- a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 
 import FormLabel from "../components/FormLabel.jsx";
diff --git a/frontend/src/metabase/admin/people/components/AddRow.jsx b/frontend/src/metabase/admin/people/components/AddRow.jsx
index a33568fae6a93673cada34e8449f010c331855fe..e0a6a58ba289d4af3488199664ead59866a35334 100644
--- a/frontend/src/metabase/admin/people/components/AddRow.jsx
+++ b/frontend/src/metabase/admin/people/components/AddRow.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/admin/people/components/EditUserForm.jsx b/frontend/src/metabase/admin/people/components/EditUserForm.jsx
index 6ae4e222ce3c41560fb725b13666bb59d8a65f09..b5280a9980cb029908a7cdc911e29f7aacaaa364 100644
--- a/frontend/src/metabase/admin/people/components/EditUserForm.jsx
+++ b/frontend/src/metabase/admin/people/components/EditUserForm.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import FormField from "metabase/components/form/FormField.jsx";
diff --git a/frontend/src/metabase/admin/people/components/GroupSelect.jsx b/frontend/src/metabase/admin/people/components/GroupSelect.jsx
index 69815705a72fe7ba2c761c568ffa1de19b1e8970..e92435ef8fdabb1962dd60072e1d38c5cdde7e36 100644
--- a/frontend/src/metabase/admin/people/components/GroupSelect.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupSelect.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import CheckBox from "metabase/components/CheckBox.jsx";
 
diff --git a/frontend/src/metabase/admin/people/components/GroupSummary.jsx b/frontend/src/metabase/admin/people/components/GroupSummary.jsx
index 1ed9e3d643297607e38f50a07cf0428d5b1bc661..13e9a22e024d1db7dc57e3d293815056c4a208f6 100644
--- a/frontend/src/metabase/admin/people/components/GroupSummary.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupSummary.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx b/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx
index 7d6108a84987472e51b0e11d5024cea1354a1eb2..ce88849accf75385b4c829d38f18185acbd48d3e 100644
--- a/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx
+++ b/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import MetabaseSettings from "metabase/lib/settings";
diff --git a/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx b/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx
index cffd89efa0c5eba3570398618284fdbf4941b01e..b8e835c5b9426b60a90a541d2ae380c7b2063d86 100644
--- a/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx
+++ b/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx
index 1ea8e2fc98dc151b9a1e4a87a0cb41b4eda08cb3..c08dd3678f592853a466233639d2a949813b3af2 100644
--- a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import { LeftNavPane, LeftNavPaneItem } from "metabase/components/LeftNavPane.jsx";
 
diff --git a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
index c306b66c19fb0e0f3bea3f54a890a94e2199f618..408f9c77a93b6fc7f8c1f0696d44813d9f1a327a 100644
--- a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import _ from "underscore";
 import { connect } from "react-redux";
diff --git a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx
index 4de3c687ea7ff17a5409dc65e245637aea597133..205a6e09307b50a56cac6eca799970919bbe509a 100644
--- a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx
+++ b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx
@@ -1,6 +1,6 @@
 /* eslint-disable react/display-name */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { Grid, ScrollSync } from 'react-virtualized'
 import 'react-virtualized/styles.css';
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx
index 92abdcaa9d67acb0070b8a0b327f8919b0f9c377..782974f644d889eedac58d2e2e99e8964be21b04 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { inflect } from "metabase/lib/formatting";
 
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx
index b9ad99a77e491846b8c54f50b04935720093326c..a35dcf3310070bee770ce208458f2152e35fc334 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import Confirm from "metabase/components/Confirm.jsx";
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx
index cad0652cc54cdfe12f3b0daa55d499184f5170f1..5e9dfe6bf15f38afaba310f621b71212412d480a 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx
@@ -1,6 +1,6 @@
 /* eslint-disable react/display-name */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { Link } from "react-router";
 
diff --git a/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx
index 7e182b4de13545392ae959fe239ba70bbae08d33..585d002b56e20fe08ef0296bb77d8f18f191b215 100644
--- a/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
diff --git a/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx
index 823d6a46f979650ed70e1d3fe77bfd92de05dd3e..9cc8f3a18c0dce61951dfaea543326867ff59f81 100644
--- a/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux"
 
 import PermissionsApp from "./PermissionsApp.jsx";
diff --git a/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx
index 3614f63a52fd6e57563889c068888c011983a19a..6a58c07a5fe1d73effdc9789f4074c11db4a59a6 100644
--- a/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx
@@ -1,4 +1,3 @@
-import React, { Component, PropTypes } from "react";
 import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
diff --git a/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx
index f4afd2bb54eab5cc306ef6240d03de9faeec367c..47bb661672719eec9fc4ef0c9ce99e1f0903daac 100644
--- a/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { withRouter } from "react-router";
 import { connect } from "react-redux"
 import { push } from "react-router-redux";
diff --git a/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx
index fdcbed3a8ebed55bb3a809f82a1147838779a6d0..744ed2d9e55e3d6af448a665565c8b15c07af414 100644
--- a/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx
@@ -1,4 +1,3 @@
-import React, { Component, PropTypes } from "react";
 import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
diff --git a/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx
index 43216ff56a16ccb9502d0c4abdb33be51c707905..31ad4e730a4f722437a4e0979d83cb16e64499e6 100644
--- a/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx
@@ -1,4 +1,3 @@
-import React, { Component, PropTypes } from "react";
 import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
diff --git a/frontend/src/metabase/admin/permissions/routes.jsx b/frontend/src/metabase/admin/permissions/routes.jsx
index 2b976fe0dcb8495870b2d628f7e8ce956dfd51cb..47308bb5699934df97207a1de4e079202407ddd8 100644
--- a/frontend/src/metabase/admin/permissions/routes.jsx
+++ b/frontend/src/metabase/admin/permissions/routes.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 import { Route, IndexRedirect } from 'react-router';
 
 import DataPermissionsApp from "./containers/DataPermissionsApp.jsx";
diff --git a/frontend/src/metabase/admin/permissions/selectors.js b/frontend/src/metabase/admin/permissions/selectors.js
index 5d188ddb0dac335ee6fbd87be4257bb3b0db0901..85510bee59995fd0d021deae11d1dfd3e9201eef 100644
--- a/frontend/src/metabase/admin/permissions/selectors.js
+++ b/frontend/src/metabase/admin/permissions/selectors.js
@@ -43,7 +43,7 @@ const SPECIAL_GROUP_FILTERS = [isAdminGroup, isDefaultGroup, isMetaBotGroup].rev
 
 function getTooltipForGroup(group) {
     if (isAdminGroup(group)) {
-        return "Administrators always have the highest level of acess to everything in Metabase."
+        return "Administrators always have the highest level of access to everything in Metabase."
     } else if (isDefaultGroup(group)) {
         return "Every Metabase user belongs to the All Users group. If you want to limit or restrict a group's access to something, make sure the All Users group has an equal or lower level of access.";
     } else if (isMetaBotGroup(group)) {
diff --git a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
index f3f4451b123ead08a4747a32f6ec992f3b5f63db..de3639693cb4af2697575517e404cb079693df46 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
index a1827f2f27765897d54d3485191b0f78d44f3fbf..8e5498b759b80f4d52b3b19588a7084c33d1ccd3 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
@@ -1,8 +1,10 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import SettingHeader from "./SettingHeader.jsx";
 
 import SettingInput from "./widgets/SettingInput.jsx";
+import SettingNumber from "./widgets/SettingNumber.jsx";
 import SettingPassword from "./widgets/SettingPassword.jsx";
 import SettingRadio from "./widgets/SettingRadio.jsx";
 import SettingToggle from "./widgets/SettingToggle.jsx";
@@ -10,6 +12,7 @@ import SettingSelect from "./widgets/SettingSelect.jsx";
 
 const SETTING_WIDGET_MAP = {
     "string":   SettingInput,
+    "number":   SettingNumber,
     "password": SettingPassword,
     "select":   SettingSelect,
     "radio":    SettingRadio,
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
index 111727d889a93f047e8b7861256fe1d3967b3c8a..b6b58ff722f001194d5e491afccd265dc3065511 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 import Icon from "metabase/components/Icon.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx
index c6606d9d0f325ff97e2f4c5290255b44de98bfd6..722f5e81bc205c08ffaca4f30672101cae902c66 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 import _ from "underscore";
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
index ba5fe3c053b3f9f6fe54abe5ea33048a2e0c41b4..fb111c719574dc4977fa6a391735cc6b18ef4e56 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseUtils from "metabase/lib/utils";
diff --git a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
index bb27567c535793e72027d9e4c57beb5da0d28ce3..1b137d567f2aaa9a04b20395e11a6ff6d883ec87 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetabaseSettings from "metabase/lib/settings";
 import MetabaseUtils from "metabase/lib/utils";
diff --git a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
index c36cda10147a25d18ea928c93a7f4d1a30226093..5308fcd1cd408de0d28ea7ed4a9d872750b7403c 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Utils from "metabase/lib/utils";
 
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
index 109b043fa9f1bfe3e9a8295e6c5472340a6a0366..892ba549ca6aeb38110a651b9fa711153f6b7e2c 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon";
 import Link from "metabase/components/Link";
@@ -9,7 +9,7 @@ import Confirm from "metabase/components/Confirm";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
 import { CardApi, DashboardApi } from "metabase/services";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
@@ -161,7 +161,7 @@ export const PublicLinksQuestionListing = () =>
         load={CardApi.listPublic}
         revoke={CardApi.deletePublicLink}
         type='Public Card Listing'
-        getUrl={({ id }) => Urls.card(id)}
+        getUrl={({ id }) => Urls.question(id)}
         getPublicUrl={({ public_uuid }) => window.location.origin + Urls.publicCard(public_uuid)}
         noLinksMessage="No questions have been publicly shared yet."
     />;
@@ -177,7 +177,7 @@ export const EmbeddedDashboardListing = () =>
 export const EmbeddedQuestionListing = () =>
     <PublicLinksListing
         load={CardApi.listEmbeddable}
-        getUrl={({ id }) => Urls.card(id)}
+        getUrl={({ id }) => Urls.question(id)}
         type='Embedded Card Listing'
         noLinksMessage="No questions have been embedded yet."
     />;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
index c4b066484f8f74a17a5a754d994146b8e81686c1..192e2b305c0ac13f82a81cd5db29eaf93175c8ad 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import SettingInput from "./SettingInput";
 import Button from "metabase/components/Button";
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..05fd4da0445808f4c7febc128deb46da6df01bd0
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+
+import SettingInput from "./SettingInput";
+
+const SettingNumber = ({ type = "number", ...props }) =>
+    <SettingInput {...props} type="number" />
+
+export default SettingNumber;
diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
index 90d818639cda26de2b30555145fa6625199a9307..0928a1420d60e83ec6c58c92eec2ed837016f83b 100644
--- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
+++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import { connect } from "react-redux";
 import MetabaseAnalytics from "metabase/lib/analytics";
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index 1f620e2ba88a90be895262ebfe397f88c5861871..ae501e697ee409d3393db53d75673754fa40a973 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -86,7 +86,7 @@ const SECTIONS = [
                 key: "email-smtp-port",
                 display_name: "SMTP Port",
                 placeholder: "587",
-                type: "string",
+                type: "number",
                 required: true,
                 validations: [["integer", "That's not a valid port number"]]
             },
@@ -236,6 +236,34 @@ const SECTIONS = [
                 getHidden: (settings) => !settings["enable-embedding"]
             }
         ]
+    },
+    {
+        name: "Caching",
+        settings: [
+            {
+                key: "enable-query-caching",
+                display_name: "Enable Caching",
+                type: "boolean"
+            },
+            {
+                key: "query-caching-min-ttl",
+                display_name: "Minimum Query Duration",
+                type: "number",
+                getHidden: (settings) => !settings["enable-query-caching"]
+            },
+            {
+                key: "query-caching-ttl-ratio",
+                display_name: "Cache Time-To-Live (TTL)",
+                type: "number",
+                getHidden: (settings) => !settings["enable-query-caching"]
+            },
+            {
+                key: "query-caching-max-kb",
+                display_name: "Max Cache Entry Size",
+                type: "number",
+                getHidden: (settings) => !settings["enable-query-caching"]
+            }
+        ]
     }
 ];
 for (const section of SECTIONS) {
diff --git a/frontend/src/metabase/auth/components/AuthScene.jsx b/frontend/src/metabase/auth/components/AuthScene.jsx
index 21f84fb783316a65c43091650b8b189574f48c33..5a00cebce7863e68a0aa865cab3eea906ad2dea8 100644
--- a/frontend/src/metabase/auth/components/AuthScene.jsx
+++ b/frontend/src/metabase/auth/components/AuthScene.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 export default class AuthScene extends Component {
diff --git a/frontend/src/metabase/auth/components/SSOLoginButton.jsx b/frontend/src/metabase/auth/components/SSOLoginButton.jsx
index 91cb3b72fd66e0756302b241495280754fb3c10a..7af959a1a02d070b58eff7922fd55ba64da5dbe7 100644
--- a/frontend/src/metabase/auth/components/SSOLoginButton.jsx
+++ b/frontend/src/metabase/auth/components/SSOLoginButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon";
 import { capitalize } from 'humanize'
 
diff --git a/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx b/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx
index f9bce741aa031cbd4314e70fe1de1c7424ff8e1e..0c51a8a90fd448a31f906868fdfd9e6400f60e94 100644
--- a/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx
+++ b/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import _ from "underscore";
 import cx from "classnames";
diff --git a/frontend/src/metabase/auth/containers/LoginApp.jsx b/frontend/src/metabase/auth/containers/LoginApp.jsx
index 1223d3c25c1fbbb75ea1454a7770344a21bdb3eb..229febd771c0a21a04db540746c7fe5a041aeb16 100644
--- a/frontend/src/metabase/auth/containers/LoginApp.jsx
+++ b/frontend/src/metabase/auth/containers/LoginApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { findDOMNode } from "react-dom";
 import { Link } from "react-router";
 import { connect } from "react-redux";
diff --git a/frontend/src/metabase/auth/containers/LogoutApp.jsx b/frontend/src/metabase/auth/containers/LogoutApp.jsx
index c7ac8276c83d8f19243087ed6910444156fc8b10..5c42f34868d43fecd1eaa9194ffba54d7e023899 100644
--- a/frontend/src/metabase/auth/containers/LogoutApp.jsx
+++ b/frontend/src/metabase/auth/containers/LogoutApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import { Component } from "react";
 import { connect } from "react-redux";
 
 import { logout } from "../auth";
diff --git a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
index 2db47592cacd1d7328c563f8123e158a8f0005d5..cc2aeb724fcd101d0d0801fc741633b02d011e2f 100644
--- a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
+++ b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { Link } from "react-router";
 
diff --git a/frontend/src/metabase/components/AccordianList.jsx b/frontend/src/metabase/components/AccordianList.jsx
index 953be139c289362f20d0024ae4d7698ed6e4abd6..64fb9752e73736eab212b5571c88838179782517 100644
--- a/frontend/src/metabase/components/AccordianList.jsx
+++ b/frontend/src/metabase/components/AccordianList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import cx from "classnames";
diff --git a/frontend/src/metabase/components/ActionButton.jsx b/frontend/src/metabase/components/ActionButton.jsx
index 465fd693c6b50dca5656caac3899d8587748f731..168d2c71a6ee66d8c4c2f64f4258cf2f1b677033 100644
--- a/frontend/src/metabase/components/ActionButton.jsx
+++ b/frontend/src/metabase/components/ActionButton.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
diff --git a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx b/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
index 408cb5777523ee39bdb95e3a7bce27f553055a8e..64d6ef600c945e7326c547cbe7e48b15ff454663 100644
--- a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
+++ b/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
@@ -1,11 +1,12 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx';
 import Icon from 'metabase/components/Icon.jsx';
 import ModalContent from "metabase/components/ModalContent.jsx";
 import SortableItemList from 'metabase/components/SortableItemList.jsx';
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 import { DashboardApi } from "metabase/services";
 
 import moment from 'moment';
diff --git a/frontend/src/metabase/components/AdminEmptyText.jsx b/frontend/src/metabase/components/AdminEmptyText.jsx
index 727b17f71e833f5d6aa322cf0c7a71c71d2e2095..a9aec2383bc565e9efa277e5aa95749c71accc1d 100644
--- a/frontend/src/metabase/components/AdminEmptyText.jsx
+++ b/frontend/src/metabase/components/AdminEmptyText.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 const AdminEmptyText = ({ message }) =>
     <h2 className="text-grey-3">{message}</h2>
diff --git a/frontend/src/metabase/components/AdminHeader.jsx b/frontend/src/metabase/components/AdminHeader.jsx
index ea413d439a4022d02ea7047e91c934b65986804d..c2507bb5f740839c0ac767b9ecd1605944281748 100644
--- a/frontend/src/metabase/components/AdminHeader.jsx
+++ b/frontend/src/metabase/components/AdminHeader.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import SaveStatus from "metabase/components/SaveStatus.jsx";
 
diff --git a/frontend/src/metabase/components/AdminLayout.jsx b/frontend/src/metabase/components/AdminLayout.jsx
index 697281025d930647096ed184393ac795511608f8..8dff1b7fe84be3c8604837ec22c58ac6682e4cb2 100644
--- a/frontend/src/metabase/components/AdminLayout.jsx
+++ b/frontend/src/metabase/components/AdminLayout.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import AdminHeader from "./AdminHeader.jsx";
 
diff --git a/frontend/src/metabase/components/AdminPaneLayout.jsx b/frontend/src/metabase/components/AdminPaneLayout.jsx
index 5709b31eaa3c50ceb943ff3c75bb2b6cabc3ae88..11cae74b5d1275776ec5685679cf9012c76b9e9e 100644
--- a/frontend/src/metabase/components/AdminPaneLayout.jsx
+++ b/frontend/src/metabase/components/AdminPaneLayout.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/components/Alert.jsx b/frontend/src/metabase/components/Alert.jsx
index c9ee25c3bb51f163be88e7b620402ddbaa5d4932..5fc912ca84b1d3c5953395b223ccdc4240f79942 100644
--- a/frontend/src/metabase/components/Alert.jsx
+++ b/frontend/src/metabase/components/Alert.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Modal from "metabase/components/Modal.jsx";
 
diff --git a/frontend/src/metabase/components/BodyComponent.jsx b/frontend/src/metabase/components/BodyComponent.jsx
index 11de27256f1648df07a49c7c7877c9939f7bb463..b14e7b8e15f19e8ed062c510fdea0d621340068a 100644
--- a/frontend/src/metabase/components/BodyComponent.jsx
+++ b/frontend/src/metabase/components/BodyComponent.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 export default ComposedComponent => class extends Component {
diff --git a/frontend/src/metabase/components/Breadcrumbs.jsx b/frontend/src/metabase/components/Breadcrumbs.jsx
index 9931219ba4dfdf8165eda1bf1df3d655f1706597..c4b10537b02b798f920b970f3d8396baaea053e4 100644
--- a/frontend/src/metabase/components/Breadcrumbs.jsx
+++ b/frontend/src/metabase/components/Breadcrumbs.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import S from "./Breadcrumbs.css";
diff --git a/frontend/src/metabase/components/Button.jsx b/frontend/src/metabase/components/Button.jsx
index bb8eb9f5a383b4de62596efba06a1cdd2cd49c6d..0bea4d0c9481528eaf488483b25c141736e0e98b 100644
--- a/frontend/src/metabase/components/Button.jsx
+++ b/frontend/src/metabase/components/Button.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/components/ButtonBar.jsx b/frontend/src/metabase/components/ButtonBar.jsx
index 851689df07061ce19a98534a511009e6d6dfad69..24381f8eac5e5cba064f43e3386aa5ebf0a3924d 100644
--- a/frontend/src/metabase/components/ButtonBar.jsx
+++ b/frontend/src/metabase/components/ButtonBar.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 
 export default class ButtonBar extends Component {
diff --git a/frontend/src/metabase/components/Calendar.jsx b/frontend/src/metabase/components/Calendar.jsx
index d3b16c32f139ff32148120ca6c12c32ab8d95f2b..6715dcb129dcaeca413aceed6cd914540f0a5fcd 100644
--- a/frontend/src/metabase/components/Calendar.jsx
+++ b/frontend/src/metabase/components/Calendar.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 
 import "./Calendar.css";
 
diff --git a/frontend/src/metabase/components/CheckBox.jsx b/frontend/src/metabase/components/CheckBox.jsx
index c22ec977a378c7354ef58064e2071c292efe8bdd..d215052027bd764feb8d3a3ab2b567a258e71330 100644
--- a/frontend/src/metabase/components/CheckBox.jsx
+++ b/frontend/src/metabase/components/CheckBox.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon.jsx";
 
 import cx from "classnames";
diff --git a/frontend/src/metabase/components/Code.jsx b/frontend/src/metabase/components/Code.jsx
index 53364f56b49edca86783e67d4688cb398e78819f..b262c0683aa68006f2d21e5ae860c850000db4da 100644
--- a/frontend/src/metabase/components/Code.jsx
+++ b/frontend/src/metabase/components/Code.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 const Code = ({ children, block }) => {
     if (block) {
diff --git a/frontend/src/metabase/components/ColorPicker.jsx b/frontend/src/metabase/components/ColorPicker.jsx
index 076c097e7c50f88d74a744f6c1a070fb9d6b571f..272ed65b5a832e4d058e540e9b4e0292e8f4fc41 100644
--- a/frontend/src/metabase/components/ColorPicker.jsx
+++ b/frontend/src/metabase/components/ColorPicker.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 
diff --git a/frontend/src/metabase/components/ColumnarSelector.jsx b/frontend/src/metabase/components/ColumnarSelector.jsx
index 13512c863b4e221ed152963b8a4af8e0828e2167..e456a839cc886f7154ce7456763c69b23fc22361 100644
--- a/frontend/src/metabase/components/ColumnarSelector.jsx
+++ b/frontend/src/metabase/components/ColumnarSelector.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import "./ColumnarSelector.css";
 
diff --git a/frontend/src/metabase/components/Confirm.jsx b/frontend/src/metabase/components/Confirm.jsx
index 79e9733e599721befca7d101dd0b1a8ea8314480..ca10c75a0508f6dcbb9561392518f92f113737e5 100644
--- a/frontend/src/metabase/components/Confirm.jsx
+++ b/frontend/src/metabase/components/Confirm.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ConfirmContent from "./ConfirmContent.jsx";
diff --git a/frontend/src/metabase/components/ConfirmContent.jsx b/frontend/src/metabase/components/ConfirmContent.jsx
index 984d420db23c298abd2da2dc6f37e965ceb11dea..64234914d058b63f3099b71969c68dffbd572f19 100644
--- a/frontend/src/metabase/components/ConfirmContent.jsx
+++ b/frontend/src/metabase/components/ConfirmContent.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
diff --git a/frontend/src/metabase/components/ConstrainToScreen.jsx b/frontend/src/metabase/components/ConstrainToScreen.jsx
index 52b6956b27c9b6fa61041d17a42b8f0b656db450..354878bbc8ffb0e94c35b38c10e2e4e029facd70 100644
--- a/frontend/src/metabase/components/ConstrainToScreen.jsx
+++ b/frontend/src/metabase/components/ConstrainToScreen.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import { constrainToScreen } from "metabase/lib/dom";
diff --git a/frontend/src/metabase/components/CopyButton.jsx b/frontend/src/metabase/components/CopyButton.jsx
index 7d2ef27b5f6d13f120d5a3870638a470d1c55365..76a51cbf5387694ebe4c529773908a2c75ff229a 100644
--- a/frontend/src/metabase/components/CopyButton.jsx
+++ b/frontend/src/metabase/components/CopyButton.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon";
 import Tooltip from "metabase/components/Tooltip";
diff --git a/frontend/src/metabase/components/CopyWidget.jsx b/frontend/src/metabase/components/CopyWidget.jsx
index 7006b591f9bd5dde63acd68e9fae73a5c9e2e2bf..01a3b4854073bfe0fb3e9482b543fbd84020f392 100644
--- a/frontend/src/metabase/components/CopyWidget.jsx
+++ b/frontend/src/metabase/components/CopyWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import CopyButton from "./CopyButton";
 
diff --git a/frontend/src/metabase/components/CreateDashboardModal.jsx b/frontend/src/metabase/components/CreateDashboardModal.jsx
index e39425df144470eefa1301fc06f55246d34c542f..58a9141cf05ef3c22005be481589442a72ed90c4 100644
--- a/frontend/src/metabase/components/CreateDashboardModal.jsx
+++ b/frontend/src/metabase/components/CreateDashboardModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import FormField from "metabase/components/FormField.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
diff --git a/frontend/src/metabase/components/DatabaseDetailsForm.jsx b/frontend/src/metabase/components/DatabaseDetailsForm.jsx
index 8a8e26de7835b6fdb06a8f1fb372d5e48fcf6306..65bf744eafab904554bfd95945ef94c3b32ba8ee 100644
--- a/frontend/src/metabase/components/DatabaseDetailsForm.jsx
+++ b/frontend/src/metabase/components/DatabaseDetailsForm.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 import FormField from "metabase/components/form/FormField.jsx";
diff --git a/frontend/src/metabase/components/DeleteModalWithConfirm.jsx b/frontend/src/metabase/components/DeleteModalWithConfirm.jsx
index d4380609900deb313087c0b3b025221dff9d78dc..a0009f137748a89d48364b794b62928056de4639 100644
--- a/frontend/src/metabase/components/DeleteModalWithConfirm.jsx
+++ b/frontend/src/metabase/components/DeleteModalWithConfirm.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 import CheckBox from "metabase/components/CheckBox.jsx";
diff --git a/frontend/src/metabase/components/DeleteQuestionModal.jsx b/frontend/src/metabase/components/DeleteQuestionModal.jsx
index 9f9977ba2ae972c05681d032d5adbf020c4478cf..510693b1fa80724a1a6b673a548934652c1737d1 100644
--- a/frontend/src/metabase/components/DeleteQuestionModal.jsx
+++ b/frontend/src/metabase/components/DeleteQuestionModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
diff --git a/frontend/src/metabase/components/DisclosureTriangle.jsx b/frontend/src/metabase/components/DisclosureTriangle.jsx
index 25eba51de80fc7e33e620b9218bc1378b0e8f7c8..908286bc03a9af78187d1df2f414b59439cbee56 100644
--- a/frontend/src/metabase/components/DisclosureTriangle.jsx
+++ b/frontend/src/metabase/components/DisclosureTriangle.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 import { Motion, spring, presets } from "react-motion";
 
 import Icon from "metabase/components/Icon";
diff --git a/frontend/src/metabase/components/DownloadButton.jsx b/frontend/src/metabase/components/DownloadButton.jsx
index 2095f254563529a1ecf8d76f3a6b300885a7edc1..e6d45c3871ef037dde4163b0de33369eb3dca6f5 100644
--- a/frontend/src/metabase/components/DownloadButton.jsx
+++ b/frontend/src/metabase/components/DownloadButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import Button from "metabase/components/Button.jsx";
 
diff --git a/frontend/src/metabase/components/EditBar.jsx b/frontend/src/metabase/components/EditBar.jsx
index 3069c0d8d88a771d6dc1a083cad165640a57e4c7..9f18730d62548d02446ed26b5456dcd0dd36c0ff 100644
--- a/frontend/src/metabase/components/EditBar.jsx
+++ b/frontend/src/metabase/components/EditBar.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 class EditBar extends Component {
diff --git a/frontend/src/metabase/components/Ellipsified.jsx b/frontend/src/metabase/components/Ellipsified.jsx
index a6c2718dd2e97ca0f5bdd6fcfc315238b47af4c6..5162a76a644084013b8193d39edb1667c89096a1 100644
--- a/frontend/src/metabase/components/Ellipsified.jsx
+++ b/frontend/src/metabase/components/Ellipsified.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import Tooltip from "metabase/components/Tooltip.jsx";
diff --git a/frontend/src/metabase/components/EmojiIcon.jsx b/frontend/src/metabase/components/EmojiIcon.jsx
index 3256aeec840336c7d33df5b5d4fc09607ee8cc1d..fd1a0d0e23ed726da158a59de4f8a24fbeefed8a 100644
--- a/frontend/src/metabase/components/EmojiIcon.jsx
+++ b/frontend/src/metabase/components/EmojiIcon.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import { emoji } from "metabase/lib/emoji";
 
diff --git a/frontend/src/metabase/components/EmptyState.jsx b/frontend/src/metabase/components/EmptyState.jsx
index a2e16089ca21859f261bd2ee5637c9893b257bdc..323cdb10afc4b0c79251838e7ca1237144d6d3b8 100644
--- a/frontend/src/metabase/components/EmptyState.jsx
+++ b/frontend/src/metabase/components/EmptyState.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/components/Expandable.jsx b/frontend/src/metabase/components/Expandable.jsx
index f921bde99352074b612009e367948fc4d638999b..256eeb20c99e0c29ae90bacfc7bd2833170d1c31 100644
--- a/frontend/src/metabase/components/Expandable.jsx
+++ b/frontend/src/metabase/components/Expandable.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 const Expandable = (ComposedComponent) => class extends Component {
     static displayName = "Expandable["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
diff --git a/frontend/src/metabase/components/ExplicitSize.jsx b/frontend/src/metabase/components/ExplicitSize.jsx
index be7c380b4457ddf6107cabe9086ac709d0fc1969..9d10e95f74f14cef9c10ce22b77ebd63446d3ac1 100644
--- a/frontend/src/metabase/components/ExplicitSize.jsx
+++ b/frontend/src/metabase/components/ExplicitSize.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import ResizeObserver from "resize-observer-polyfill";
diff --git a/frontend/src/metabase/components/FieldSet.jsx b/frontend/src/metabase/components/FieldSet.jsx
index d73e60dc5af0e35fab8494a6de0275e4173a75f7..2e787403e5aefdefb2385bb11b820107bf942af4 100644
--- a/frontend/src/metabase/components/FieldSet.jsx
+++ b/frontend/src/metabase/components/FieldSet.jsx
@@ -1,4 +1,4 @@
-import React, {Component, PropTypes} from "react";
+import React from "react";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/components/FormField.jsx b/frontend/src/metabase/components/FormField.jsx
index 3000b231d68f6f245a8dfec10905c3b07fd3d1e2..a615e91d64afa863976f1ff822dc717acb07dd41 100644
--- a/frontend/src/metabase/components/FormField.jsx
+++ b/frontend/src/metabase/components/FormField.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/components/Header.jsx b/frontend/src/metabase/components/Header.jsx
index 2f16bfbb78da0113f897fd0c794fb3cb16e3c68f..f8948586f987b91aaefe5fd4697a4b147d6bd136 100644
--- a/frontend/src/metabase/components/Header.jsx
+++ b/frontend/src/metabase/components/Header.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import Input from "metabase/components/Input.jsx";
diff --git a/frontend/src/metabase/components/HeaderBar.jsx b/frontend/src/metabase/components/HeaderBar.jsx
index 5a67451b39f67b4906b083486018f5df22e6f04e..c82d1a23020f8c26cf0150d3c62ad054842d7171 100644
--- a/frontend/src/metabase/components/HeaderBar.jsx
+++ b/frontend/src/metabase/components/HeaderBar.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Input from "metabase/components/Input.jsx";
 import TitleAndDescription from "metabase/components/TitleAndDescription.jsx";
diff --git a/frontend/src/metabase/components/HeaderModal.jsx b/frontend/src/metabase/components/HeaderModal.jsx
index 16b18c3ea40ff87d6cc0d34bcf76b5d9b3b24a7e..6de586575247a16b3a28f54f4c1a39441bf8afcc 100644
--- a/frontend/src/metabase/components/HeaderModal.jsx
+++ b/frontend/src/metabase/components/HeaderModal.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import BodyComponent from "metabase/components/BodyComponent";
 import cx from "classnames";
diff --git a/frontend/src/metabase/components/HeaderWithBack.jsx b/frontend/src/metabase/components/HeaderWithBack.jsx
index 6f5761adfc0bf1f90ca26cf8d70ab74d321e8fb6..b3fbfbf2b889d6c4a7533028f04f1dfa70025eee 100644
--- a/frontend/src/metabase/components/HeaderWithBack.jsx
+++ b/frontend/src/metabase/components/HeaderWithBack.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon";
 import TitleAndDescription from "metabase/components/TitleAndDescription";
diff --git a/frontend/src/metabase/components/HistoryModal.jsx b/frontend/src/metabase/components/HistoryModal.jsx
index 82fcb3bf48e2ca99e38dbb6bd71f88fc8144402a..3b545724611c3afbb110586d732256c215b38da5 100644
--- a/frontend/src/metabase/components/HistoryModal.jsx
+++ b/frontend/src/metabase/components/HistoryModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
@@ -72,7 +73,7 @@ export default class HistoryModal extends Component {
         var { revisions } = this.props;
         return (
             <ModalContent
-                title="Change History"
+                title="Revision history"
                 onClose={() => this.props.onClose()}
             >
                 <LoadingAndErrorWrapper loading={!revisions} error={this.state.error}>
diff --git a/frontend/src/metabase/components/Icon.jsx b/frontend/src/metabase/components/Icon.jsx
index 847e1cc131e6d55aa169f0db8eb9a0dddd9e92f9..ff6078af9f0ffc2f3e62a34d85a5298e40984fa9 100644
--- a/frontend/src/metabase/components/Icon.jsx
+++ b/frontend/src/metabase/components/Icon.jsx
@@ -1,6 +1,7 @@
 /*eslint-disable react/no-danger */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import RetinaImage from "react-retina-image";
 
 import { loadIcon } from 'metabase/icon_paths';
diff --git a/frontend/src/metabase/components/IconBorder.jsx b/frontend/src/metabase/components/IconBorder.jsx
index b23a29bf1655bd73df445e5ac7962f405d20abab..f249b505b2a49de2c2b6e63d5acd9c940d4cf783 100644
--- a/frontend/src/metabase/components/IconBorder.jsx
+++ b/frontend/src/metabase/components/IconBorder.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 /*
diff --git a/frontend/src/metabase/components/Input.jsx b/frontend/src/metabase/components/Input.jsx
index bfef3a9a2988d52d906adb5dd318cd0eaa403e5d..9d754421d076894331c8f61fc03504560bc56667 100644
--- a/frontend/src/metabase/components/Input.jsx
+++ b/frontend/src/metabase/components/Input.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/components/LabelIcon.jsx b/frontend/src/metabase/components/LabelIcon.jsx
index 51ce497119276171807674ab91a8b8cd4e0e589d..b62b0752479e5f49ecc6358b426a04dc51fc9390 100644
--- a/frontend/src/metabase/components/LabelIcon.jsx
+++ b/frontend/src/metabase/components/LabelIcon.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import S from "./LabelIcon.css";
 
diff --git a/frontend/src/metabase/components/List.jsx b/frontend/src/metabase/components/List.jsx
index 407cafe8b88ad608d7073307690824cf6808a8a4..047518a04c3ab389e81dad992853b198fbf3b430 100644
--- a/frontend/src/metabase/components/List.jsx
+++ b/frontend/src/metabase/components/List.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import S from "./List.css";
 import pure from "recompose/pure";
diff --git a/frontend/src/metabase/components/ListItem.jsx b/frontend/src/metabase/components/ListItem.jsx
index f7e15914a6b7db38f112aa2d87a191652e950f45..6082dc558bd8b6e04829c5ec77f816d83ace479a 100644
--- a/frontend/src/metabase/components/ListItem.jsx
+++ b/frontend/src/metabase/components/ListItem.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./List.css";
 
diff --git a/frontend/src/metabase/components/ListSearchField.jsx b/frontend/src/metabase/components/ListSearchField.jsx
index 0783744264ab81252a98f5f08f8582d70dddd429..6025da72aacd978db8625c02fc901897b9cf288c 100644
--- a/frontend/src/metabase/components/ListSearchField.jsx
+++ b/frontend/src/metabase/components/ListSearchField.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
index fd4c7df84662c17f003b7903ae6a853ea28ab591..a0fc95eae3c38bc1107a25b7cd74f5569dfd811c 100644
--- a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
+++ b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 
diff --git a/frontend/src/metabase/components/LoadingSpinner.jsx b/frontend/src/metabase/components/LoadingSpinner.jsx
index 8356d3d71216aea07723bef88c5951164d95d159..38a5d0a8bae99f415cf5d3b87d25d320bba36d0f 100644
--- a/frontend/src/metabase/components/LoadingSpinner.jsx
+++ b/frontend/src/metabase/components/LoadingSpinner.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import "./LoadingSpinner.css";
 
diff --git a/frontend/src/metabase/components/LogoIcon.jsx b/frontend/src/metabase/components/LogoIcon.jsx
index 15443a7e57805c18307f15997323a15551fe6435..b3134321a4f48d3a4e07a543be1d308818579b9d 100644
--- a/frontend/src/metabase/components/LogoIcon.jsx
+++ b/frontend/src/metabase/components/LogoIcon.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 export default class LogoIcon extends Component {
diff --git a/frontend/src/metabase/components/Logs.jsx b/frontend/src/metabase/components/Logs.jsx
index aa3e31572ad1b3b6c5a4c9f682cf1ad315e9e1b6..eb1521b1b217a7412375917ad9f1f5a9b0fc110d 100644
--- a/frontend/src/metabase/components/Logs.jsx
+++ b/frontend/src/metabase/components/Logs.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import fetch from 'isomorphic-fetch';
 
diff --git a/frontend/src/metabase/components/Modal.jsx b/frontend/src/metabase/components/Modal.jsx
index 592a39fc1bfb60bb20965daf9381f69c06eab7ba..04d97740acfd632b116169f0f7c4626b492199f8 100644
--- a/frontend/src/metabase/components/Modal.jsx
+++ b/frontend/src/metabase/components/Modal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/components/ModalContent.jsx b/frontend/src/metabase/components/ModalContent.jsx
index 560aca89e9de922aea4715d6943fecd6d2637c8b..51909fcdea445bb91926073cd2fb473f8aa564d8 100644
--- a/frontend/src/metabase/components/ModalContent.jsx
+++ b/frontend/src/metabase/components/ModalContent.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import { MODAL_CHILD_CONTEXT_TYPES } from "./Modal";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/components/NewsletterForm.jsx b/frontend/src/metabase/components/NewsletterForm.jsx
index 18955f11386584ca14d33e4a359165071b29fee0..55a52d563c6d2f7d04b6fb03c8cdf8531e15254f 100644
--- a/frontend/src/metabase/components/NewsletterForm.jsx
+++ b/frontend/src/metabase/components/NewsletterForm.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import Icon from 'metabase/components/Icon.jsx';
diff --git a/frontend/src/metabase/components/NotFound.jsx b/frontend/src/metabase/components/NotFound.jsx
index 0eb16a19945a2dbe89ac36d3a778b913b0820d96..08c54655ccf0cf68cb99631ced3add58f3c2b897 100644
--- a/frontend/src/metabase/components/NotFound.jsx
+++ b/frontend/src/metabase/components/NotFound.jsx
@@ -1,6 +1,8 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 
+import * as Urls from "metabase/lib/urls";
+
 export default class NotFound extends Component {
     render() {
         return (
@@ -11,7 +13,7 @@ export default class NotFound extends Component {
                     <p className="h4">You might've been tricked by a ninja, but in all likelihood, you were just given a bad link.</p>
                     <p className="h4 my4">You can always:</p>
                     <div className="flex align-center">
-                        <Link to="/q" className="Button Button--primary">
+                        <Link to={Urls.question()} className="Button Button--primary">
                             <div className="p1">Ask a new question.</div>
                         </Link>
                         <span className="mx2">or</span>
diff --git a/frontend/src/metabase/components/OnClickOutsideWrapper.jsx b/frontend/src/metabase/components/OnClickOutsideWrapper.jsx
index 7a972d73ec8c0bee92630b34eb954b946b2291bf..961382729f9324e365e1351207531c8ec5ae8c96 100644
--- a/frontend/src/metabase/components/OnClickOutsideWrapper.jsx
+++ b/frontend/src/metabase/components/OnClickOutsideWrapper.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 // keep track of the order popovers were opened so we only close the last one when clicked outside
diff --git a/frontend/src/metabase/components/PasswordReveal.jsx b/frontend/src/metabase/components/PasswordReveal.jsx
index eba44e2cc2d075b759376c8d308fe5e1c8d86a25..be1387260a99f4fff186c6a430d9f4179c3646ef 100644
--- a/frontend/src/metabase/components/PasswordReveal.jsx
+++ b/frontend/src/metabase/components/PasswordReveal.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 export default class PasswordReveal extends Component {
 
diff --git a/frontend/src/metabase/components/Popover.jsx b/frontend/src/metabase/components/Popover.jsx
index f4d255e88c3aea8ad4c6a7c5d362e331f5b45c7d..cd72579ac1558cecd4cf550494cb984c06043548 100644
--- a/frontend/src/metabase/components/Popover.jsx
+++ b/frontend/src/metabase/components/Popover.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 
diff --git a/frontend/src/metabase/components/ProgressBar.jsx b/frontend/src/metabase/components/ProgressBar.jsx
index cdc79c62f7d4d58acace1ff048b5b3a0c4b009d7..a10bf6de3685c4257ba41b8bedc1036a245bb689 100644
--- a/frontend/src/metabase/components/ProgressBar.jsx
+++ b/frontend/src/metabase/components/ProgressBar.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 export default class ProgressBar extends Component {
     static propTypes = {
diff --git a/frontend/src/metabase/components/QueryButton.jsx b/frontend/src/metabase/components/QueryButton.jsx
index 83fdb22244dfeda38d2097d0a7860a3b54309119..58985a6ae905c2ee55a6e00e4f5e7d0735d44d40 100644
--- a/frontend/src/metabase/components/QueryButton.jsx
+++ b/frontend/src/metabase/components/QueryButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import pure from "recompose/pure";
 import cx from "classnames";
@@ -39,4 +40,4 @@ QueryButton.propTypes = {
     link: PropTypes.string
 };
 
-export default pure(QueryButton);
\ No newline at end of file
+export default pure(QueryButton);
diff --git a/frontend/src/metabase/components/QuestionSavedModal.jsx b/frontend/src/metabase/components/QuestionSavedModal.jsx
index 6623904d06014358400fdffe83b2d3862acc4e00..6bcbe8486e6ea8ed97bcbb95bd57f570835b04cd 100644
--- a/frontend/src/metabase/components/QuestionSavedModal.jsx
+++ b/frontend/src/metabase/components/QuestionSavedModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
diff --git a/frontend/src/metabase/components/Radio.jsx b/frontend/src/metabase/components/Radio.jsx
index 3707ed77871e26d369809fcc23ae83ab5f60d925..0a5c7eb9c80429d334215e67eead3f01ce46a996 100644
--- a/frontend/src/metabase/components/Radio.jsx
+++ b/frontend/src/metabase/components/Radio.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 import _ from "underscore";
diff --git a/frontend/src/metabase/components/SaveStatus.jsx b/frontend/src/metabase/components/SaveStatus.jsx
index 21719c6ae7af6ad8c5b08e3bf67e7f001f7c52d2..2b6405f0c96bb8c8e37138156ac7569d5bebc5de 100644
--- a/frontend/src/metabase/components/SaveStatus.jsx
+++ b/frontend/src/metabase/components/SaveStatus.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx
index 20adc9c358a24b0216b040e7acf630be9878d727..46452638ca8020475919afe1daa823cbaed7b6a0 100644
--- a/frontend/src/metabase/components/Select.jsx
+++ b/frontend/src/metabase/components/Select.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ColumnarSelector from "metabase/components/ColumnarSelector.jsx";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/components/SelectButton.jsx b/frontend/src/metabase/components/SelectButton.jsx
index f0156b5c45269b55c13734514e16aedde32ba986..e14ed5577c0c8171bd9faf7758b5058e085a07be 100644
--- a/frontend/src/metabase/components/SelectButton.jsx
+++ b/frontend/src/metabase/components/SelectButton.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/components/ShrinkableList.jsx b/frontend/src/metabase/components/ShrinkableList.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..457841fd15c62d49a9945102ec96c4e89e03ade5
--- /dev/null
+++ b/frontend/src/metabase/components/ShrinkableList.jsx
@@ -0,0 +1,59 @@
+/* @flow */
+
+import React, { Component } from "react";
+import ReactDOM from "react-dom";
+
+import ExplicitSize from "metabase/components/ExplicitSize";
+
+type Props = {
+    className?: string,
+    items: any[],
+    renderItem: (item: any) => any,
+    renderItemSmall: (item: any) => any
+};
+
+type State = {
+    isShrunk: ?boolean
+};
+
+@ExplicitSize
+export default class ShrinkableList extends Component<*, Props, State> {
+    state: State = {
+        isShrunk: null
+    }
+
+    componentWillReceiveProps() {
+        this.setState({
+            isShrunk: null
+        })
+    }
+
+    componentDidMount() {
+        this.componentDidUpdate();
+    }
+
+    componentDidUpdate() {
+        const container = ReactDOM.findDOMNode(this)
+        const { isShrunk } = this.state;
+        if (container && isShrunk === null) {
+            this.setState({
+                isShrunk: container.scrollWidth !== container.offsetWidth
+            })
+        }
+    }
+
+    render() {
+        const { items, className, renderItemSmall, renderItem } = this.props;
+        const { isShrunk } = this.state;
+        return (
+            <div className={className}>
+                { items.map(item =>
+                    isShrunk ?
+                        renderItemSmall(item)
+                    :
+                        renderItem(item)
+                )}
+            </div>
+        );
+    }
+}
diff --git a/frontend/src/metabase/components/Sidebar.jsx b/frontend/src/metabase/components/Sidebar.jsx
index ff8983019307843cdc49e9e6fafb8f38a37a783b..2d5d89794c76150df5bde3fa377a28d7e3fbec56 100644
--- a/frontend/src/metabase/components/Sidebar.jsx
+++ b/frontend/src/metabase/components/Sidebar.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./Sidebar.css";
 
diff --git a/frontend/src/metabase/components/SidebarLayout.jsx b/frontend/src/metabase/components/SidebarLayout.jsx
index 3ad943653ccfb66c89dcd344d68e6a2f2a8872ee..6a72feeadcc938d2c852b430f77693b77c41fada 100644
--- a/frontend/src/metabase/components/SidebarLayout.jsx
+++ b/frontend/src/metabase/components/SidebarLayout.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 const SidebarLayout = ({ className, style, sidebar, children }) =>
     <div className={className} style={{ ...style, display: "flex", flexDirection: "row"}}>
diff --git a/frontend/src/metabase/components/SortableItemList.jsx b/frontend/src/metabase/components/SortableItemList.jsx
index 00495171c6ff8fe3a56c83e9f629225452409b57..417eaacacff8a95e844e15f1308af066b58a7fdc 100644
--- a/frontend/src/metabase/components/SortableItemList.jsx
+++ b/frontend/src/metabase/components/SortableItemList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import "./SortableItemList.css";
 
diff --git a/frontend/src/metabase/components/StackedCheckBox.jsx b/frontend/src/metabase/components/StackedCheckBox.jsx
index 6b44a0dbaa1a0f2ef38a20525074a46075f61d6a..0332500ed3045a0edf069d2f80b68b3d51d1fd82 100644
--- a/frontend/src/metabase/components/StackedCheckBox.jsx
+++ b/frontend/src/metabase/components/StackedCheckBox.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import CheckBox from "metabase/components/CheckBox.jsx";
 
diff --git a/frontend/src/metabase/components/TextEditor.jsx b/frontend/src/metabase/components/TextEditor.jsx
index db65a48337fb5c7b84f4b18fc373f944fae4ba68..d23fc3505aec4e4ef33e88f50cba0692e54751b6 100644
--- a/frontend/src/metabase/components/TextEditor.jsx
+++ b/frontend/src/metabase/components/TextEditor.jsx
@@ -1,6 +1,7 @@
 /*global ace*/
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import "ace/ace";
diff --git a/frontend/src/metabase/components/TitleAndDescription.jsx b/frontend/src/metabase/components/TitleAndDescription.jsx
index 81dee583aad5a0617bdc0d3ece93aee999ba9728..899de18e87ff7f913f8486f7bfd1b24df7e6f346 100644
--- a/frontend/src/metabase/components/TitleAndDescription.jsx
+++ b/frontend/src/metabase/components/TitleAndDescription.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react';
+import React from 'react';
+import PropTypes from "prop-types";
 import pure from "recompose/pure";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/components/Toggle.jsx b/frontend/src/metabase/components/Toggle.jsx
index 79f80c4d1069f86815f9297cbbd7f69a0970d941..7617f8723622bf3ba192ef9175a87a4b0756ebd9 100644
--- a/frontend/src/metabase/components/Toggle.jsx
+++ b/frontend/src/metabase/components/Toggle.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import styles from "./Toggle.css";
 
diff --git a/frontend/src/metabase/components/Tooltip.jsx b/frontend/src/metabase/components/Tooltip.jsx
index 68a84a2c6a3343882c046e3ee9bfab27ffbc4395..7b8d6f5fbb4c1adf7ba7f639195a044462f45704 100644
--- a/frontend/src/metabase/components/Tooltip.jsx
+++ b/frontend/src/metabase/components/Tooltip.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import TooltipPopover from "./TooltipPopover.jsx";
@@ -14,7 +15,7 @@ export default class Tooltip extends Component {
     }
 
     static propTypes = {
-        tooltip: PropTypes.node.isRequired,
+        tooltip: PropTypes.node,
         children: PropTypes.element.isRequired,
         isEnabled: PropTypes.bool,
         verticalAttachments: PropTypes.array,
diff --git a/frontend/src/metabase/components/TooltipPopover.jsx b/frontend/src/metabase/components/TooltipPopover.jsx
index 5bbf6c15ab6a31bbe3602e67ed8403ffafe687cc..5fceead08de609725c46ed812fc92d966d24c337 100644
--- a/frontend/src/metabase/components/TooltipPopover.jsx
+++ b/frontend/src/metabase/components/TooltipPopover.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 import cx from "classnames";
 import pure from "recompose/pure";
 
diff --git a/frontend/src/metabase/components/Triggerable.jsx b/frontend/src/metabase/components/Triggerable.jsx
index 9a4fbc67d51d8f2f13fedf23204bac63819b703d..b30d90daca10f29915e5e069866fd0caed8b5ec3 100644
--- a/frontend/src/metabase/components/Triggerable.jsx
+++ b/frontend/src/metabase/components/Triggerable.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import { isObscured } from "metabase/lib/dom";
diff --git a/frontend/src/metabase/components/Unauthorized.jsx b/frontend/src/metabase/components/Unauthorized.jsx
index 7c29bac0f7f9ed425226851485e01b4e88cbdd8e..d43c1eb98a9c87e8a86a601b2e6ce5016b87c407 100644
--- a/frontend/src/metabase/components/Unauthorized.jsx
+++ b/frontend/src/metabase/components/Unauthorized.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/components/UserAvatar.jsx b/frontend/src/metabase/components/UserAvatar.jsx
index af24b4bcf6bc6986d78330e2d1f48d0d5cf1f21b..0c002cd4d32931636033ea4ab727e69c0b27de9b 100644
--- a/frontend/src/metabase/components/UserAvatar.jsx
+++ b/frontend/src/metabase/components/UserAvatar.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import cx from 'classnames';
 
 export default class UserAvatar extends Component {
diff --git a/frontend/src/metabase/components/form/FormField.jsx b/frontend/src/metabase/components/form/FormField.jsx
index 90cf12e242f7edc03ddb73146e28fb296c4b1ae2..4e4066d20969dddfbf9b7808754712fd0b4c15cf 100644
--- a/frontend/src/metabase/components/form/FormField.jsx
+++ b/frontend/src/metabase/components/form/FormField.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/components/form/FormLabel.jsx b/frontend/src/metabase/components/form/FormLabel.jsx
index 5e79a4bb8a50ed7730b33c525f7c6b925c1fb505..5a14bed3229ad96489b1a4040543389a9e1a9149 100644
--- a/frontend/src/metabase/components/form/FormLabel.jsx
+++ b/frontend/src/metabase/components/form/FormLabel.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 
diff --git a/frontend/src/metabase/components/form/FormMessage.jsx b/frontend/src/metabase/components/form/FormMessage.jsx
index 1fead88a8a2142d2c17ba58ad76614f7cc2e8bf4..09f2c4ca15ea682dd34c66a7f62f2484c2d55637 100644
--- a/frontend/src/metabase/components/form/FormMessage.jsx
+++ b/frontend/src/metabase/components/form/FormMessage.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import cx from "classnames";
 
 
diff --git a/frontend/src/metabase/containers/SaveQuestionModal.jsx b/frontend/src/metabase/containers/SaveQuestionModal.jsx
index 6e4ed2b26b0d9220254fdf51900bb54eabdbf361..7e7dae69a8a5745ba16408ecfc3c2d09c38e79ed 100644
--- a/frontend/src/metabase/containers/SaveQuestionModal.jsx
+++ b/frontend/src/metabase/containers/SaveQuestionModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 
diff --git a/frontend/src/metabase/containers/UndoListing.jsx b/frontend/src/metabase/containers/UndoListing.jsx
index 8d1e7785a39cb51b33b74d790bdb01d20421a02a..672fcd1d86906f66496fcda45e0851e480ac17e0 100644
--- a/frontend/src/metabase/containers/UndoListing.jsx
+++ b/frontend/src/metabase/containers/UndoListing.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import S from "./UndoListing.css";
diff --git a/frontend/src/metabase/css/core/flex.css b/frontend/src/metabase/css/core/flex.css
index 9ad2d5853c9fea87147af5128d3264e0e8de72e1..f88fcf8efb97e6ba990655bf4b5468b492994915 100644
--- a/frontend/src/metabase/css/core/flex.css
+++ b/frontend/src/metabase/css/core/flex.css
@@ -32,6 +32,10 @@
     justify-content: space-between;
 }
 
+.justify-end {
+    justify-content: flex-end;
+}
+
 .align-start {
     align-items: flex-start;
 }
diff --git a/frontend/src/metabase/css/query_builder.css b/frontend/src/metabase/css/query_builder.css
index a8cb318deca637f3a5104b37fdbd098911dc7753..f298eff4caf824a7d90164dc5ec9cfb288b2c88e 100644
--- a/frontend/src/metabase/css/query_builder.css
+++ b/frontend/src/metabase/css/query_builder.css
@@ -518,12 +518,13 @@
     z-index: 1;
     opacity: 1;
     box-shadow: 0 1px 2px rgba(0, 0, 0, .22);
-    transition: margin-top 0.5s, opacity 0.5s;
+    transition: transform 0.5s, opacity 0.5s;
     min-width: 8em;
+    position: relative;
 }
 
 .RunButton.RunButton--hidden {
-    margin-top: -110px;
+    transform: translateY(-65px);
     opacity: 0;
 }
 
diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx
index 4dfbefdcadeb031a842a90661be62a8390e730f5..d373afa4b74a12f312be6c6b394e58abdd15d661 100644
--- a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx
+++ b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Visualization from "metabase/visualizations/components/Visualization.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
diff --git a/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx b/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx
index 1d513501c53df8ecf1b277ac0d76114c2a542fd3..0ab0ea01ddc693a8d45b094189192f875c624201 100644
--- a/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx
+++ b/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 import AddToDashboard from "metabase/questions/containers/AddToDashboard.jsx";
diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx
index 0ef43f0234fb8a89309a3c11d006dbd81499c650..01bb76aeff49686d006fdb854fd5b5c96630a0f5 100644
--- a/frontend/src/metabase/dashboard/components/DashCard.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCard.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import visualizations, { getVisualizationRaw } from "metabase/visualizations";
diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx
index 931f3d9969ea0b7ef2366c8923f40bc3c2876481..5325abcbdd86492c2bb0a301d9bede809b99ecc6 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard.jsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import DashboardHeader from "../components/DashboardHeader.jsx";
 import DashboardGrid from "../components/DashboardGrid.jsx";
diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
index a037198fc1b42e9a037b36d1dabe345f93f1cebe..4441f0249d1d3900b451f32908881a42ef70f5ec 100644
--- a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import GridLayout from "./grid/GridLayout.jsx";
 import DashCard from "./DashCard.jsx";
diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
index 9debed742065740077330b83776c326a7994b3f5..db1f7d977f87d32f2150aab9882267d9d519caee 100644
--- a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
 import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.jsx";
@@ -146,7 +147,7 @@ export default class DashboardHeader extends Component {
             // Parameters
             buttons.push(
                 <span>
-                    <Tooltip tooltip="Add a Filter">
+                    <Tooltip tooltip="Add a filter">
                         <a
                           key="parameters"
                           className={cx("text-brand-hover", { "text-brand": this.state.modal == "parameters" })}
@@ -173,7 +174,7 @@ export default class DashboardHeader extends Component {
                     key="history"
                     ref="dashboardHistory"
                     triggerElement={
-                        <Tooltip tooltip="Revision History">
+                        <Tooltip tooltip="Revision history">
                             <span data-metabase-event={"Dashboard;Revisions"}>
                                 <Icon className="text-brand-hover" name="history" size={16} />
                             </span>
@@ -195,7 +196,7 @@ export default class DashboardHeader extends Component {
 
         if (!isFullscreen && !isEditing && canEdit) {
             buttons.push(
-                <Tooltip tooltip="Edit Dashboard">
+                <Tooltip tooltip="Edit dashboard">
                     <a data-metabase-event="Dashboard;Edit" key="edit" title="Edit Dashboard Layout" className="text-brand-hover cursor-pointer" onClick={() => this.onEdit()}>
                         <Icon name="pencil" size={16} />
                     </a>
@@ -210,7 +211,7 @@ export default class DashboardHeader extends Component {
                     key="add"
                     ref="addQuestionModal"
                     triggerElement={
-                        <Tooltip tooltip="Add Card">
+                        <Tooltip tooltip="Add a question">
                             <span data-metabase-event="Dashboard;Add Card Modal" title="Add a question to this dashboard">
                                 <Icon className={cx("text-brand-hover cursor-pointer", { "Icon--pulse": isEmpty })} name="add" size={16} />
                             </span>
diff --git a/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx b/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx
index 653449a0899930a454017e99479d208f00af4ded..f75223b38abe756115200ad4dfa9029dfe857496 100644
--- a/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx
+++ b/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
diff --git a/frontend/src/metabase/dashboard/components/RefreshWidget.jsx b/frontend/src/metabase/dashboard/components/RefreshWidget.jsx
index 7558419887f11b33d70c2e54c16cfcd4a3090f19..afcf9cf84e6f4e4ec7579af80ab11c8b68bf42d2 100644
--- a/frontend/src/metabase/dashboard/components/RefreshWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/RefreshWidget.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import styles from "./RefreshWidget.css";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
diff --git a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
index e6d17b5fa6122f15c26c49701d9315898adf9418..b1d85a855b075c0f6649eb53458fbda733f9df95 100644
--- a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
+++ b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 import ModalContent from "metabase/components/ModalContent.jsx";
diff --git a/frontend/src/metabase/dashboard/components/grid/GridItem.jsx b/frontend/src/metabase/dashboard/components/grid/GridItem.jsx
index 481cda2928d94c1408cdd3fa6cbc94ca4060149d..6a0b745f8dce13f7cfb09a5633e50daa4c4294c9 100644
--- a/frontend/src/metabase/dashboard/components/grid/GridItem.jsx
+++ b/frontend/src/metabase/dashboard/components/grid/GridItem.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { DraggableCore } from "react-draggable";
 import { Resizable } from "react-resizable";
diff --git a/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx b/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx
index 57f9d0e365ed11d2559785d873bef99b24066232..701934e1ca19e6cbd361019f7c15b6bbff8917e5 100644
--- a/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx
+++ b/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import GridItem from "./GridItem.jsx";
diff --git a/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx b/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx
index 24d92c79fc7c0b1cd72f91354d5af46ee0614e5f..00a5bc7476a71085a655458bb948e8add226d23b 100644
--- a/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import DashCardCardParameterMapper from "../../containers/DashCardCardParameterMapper.jsx";
 
@@ -6,7 +6,7 @@ const DashCardParameterMapper = ({ dashcard }) =>
     <div className="relative flex-full flex flex-column layout-centered">
         { dashcard.series && dashcard.series.length > 0 &&
             <div className="mx4 my1 p1 rounded" style={{ backgroundColor: "#F5F5F5", color: "#8691AC", marginTop: -10 }}>
-                Make sure to make a selection for each series, or the filter won't work for the card
+                Make sure to make a selection for each series, or the filter won't work on this card.
             </div>
         }
         <div className="flex mx4 z1" style={{ justifyContent: "space-around" }}>
diff --git a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
index 4aacaf8dff48740d3752a72e4df363ff07088f3f..c7d44bf601f85dfca12787a1fff1c65bc5977dec 100644
--- a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, {Component, PropTypes} from "react"
+import React, {Component} from "react"
+import PropTypes from "prop-types";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx b/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
index d1788e09a6685d0dd315d0ecf388d9198a4db1db..636c2f92323db6b95b9b5d1fa3852171a407df27 100644
--- a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
@@ -1,5 +1,5 @@
 /* @flow */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { PARAMETER_SECTIONS } from "metabase/meta/Dashboard";
 import type { ParameterOption } from "metabase/meta/types/Dashboard";
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx
index 553f579c6c9635cd2ab99ed6cafcc4d614fa933c..1d19376aee2b7e29ddeea6c7263bb000ceb2ade5 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx
@@ -1,7 +1,8 @@
 /* @flow */
 /* eslint "react/prop-types": "warn" */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import { createMultiwordSearchRegex } from "metabase/lib/string";
 
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx
index 8c6bf8188c4d73492833bf63dbbc0eb6db3a8189..52b252b6de994e5a428cf3e21e8511fedd30dc3d 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, {Component, PropTypes} from "react";
+import React, {Component} from "react";
 import cx from "classnames";
 
 import DatePicker, {DATE_OPERATORS} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx
index 64c904c407ada9e7b2ed7eb4d548e5c16b7a69b7..a037e3576f3cc4316021a3377d7b81304c62bc00 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import YearPicker from "./YearPicker.jsx";
 
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx
index 47e3ece51266cc889a6e42258a83e61bf0f9e2be..7fb3c4549b39968ed6fb819ff59099776cdddeab 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import YearPicker from "./YearPicker.jsx";
 
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx
index fd664cc474b772bef6b5c47dbab6079cc72c2d31..b3c777f50653f537749d8182cbdaebbc40775e6a 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Calendar from "metabase/components/Calendar.jsx";
 import moment from "moment";
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx
index dfc1f98bece7aadd1c101229c11e7a373de99818..e5d865a86d0d6888a73aad46e281b64778440948 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 import _ from "underscore";
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx
index 1035cf795d40e785e0575a45426cd923f24f547e..09141e8f4e137bb4fe7b1dee6174021f97ac0265 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Calendar from "metabase/components/Calendar.jsx";
 import moment from "moment";
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx
index b26c6f929ca16caf9ba7b2ea7ec6907898b1411e..553c01eaee5ac465b771c4a5dacafac797d7b4ab 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 export default class TextWidget extends Component {
     constructor(props, context) {
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx
index df2a080279da2fd39aa388051fd91de22761fbbb..455eeb5fc466b9623184922623e87cb497db7140 100644
--- a/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Select from "metabase/components/Select.jsx";
 import _ from "underscore";
diff --git a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
index f23e115582c97416696af72db8e352b1ed14809e..f2049bfce17699c3f815077a5854b736e3dd2239 100644
--- a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import S from "./DashCardCardParameterMapper.css";
diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
index 6ec9f224767665e4b7c468e664a5dea6c20e9d85..8e704cde2cdc3225d7c6a29d626cc344f587e87d 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
diff --git a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
index 6ff0deeb50520db60ecf107be57925c318123142..cd76a516159bb1b31fe14cd12ee93e2eaf917491 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
@@ -1,11 +1,11 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams } from "../dashboard";
 
diff --git a/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx b/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx
index e352c644b130e2fc9c67233e6b8ec30905713116..cb81c3845169edfde21e6791528d0063d352d951 100644
--- a/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx
+++ b/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from "prop-types";
 import {connect} from "react-redux";
 
 import ParameterValueWidget from "../components/parameters/ParameterValueWidget.jsx";
diff --git a/frontend/src/metabase/dashboard/containers/Parameters.jsx b/frontend/src/metabase/dashboard/containers/Parameters.jsx
index c0d14c887c0ed80efbc85f2352b7f6b9c1cf45c2..27922677a5f5ab5172ed0918ac7f465698bb0861 100644
--- a/frontend/src/metabase/dashboard/containers/Parameters.jsx
+++ b/frontend/src/metabase/dashboard/containers/Parameters.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import ParameterWidget from "./ParameterWidget.jsx";
 
diff --git a/frontend/src/metabase/hoc/Routeless.jsx b/frontend/src/metabase/hoc/Routeless.jsx
index 1e2b22de0357f296d9e44a23c6aa6921efc90bb1..f231e6c2798bfb476cc904b66b09f6c7b2e1b072 100644
--- a/frontend/src/metabase/hoc/Routeless.jsx
+++ b/frontend/src/metabase/hoc/Routeless.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { connect } from "react-redux";
 import { push, goBack } from "react-router-redux";
diff --git a/frontend/src/metabase/hoc/Tooltipify.jsx b/frontend/src/metabase/hoc/Tooltipify.jsx
index e2c8a2e45f0820cb544e25fc66e7569e47adc0ba..5aaae09c1eb677ea25508976425a156de05df041 100644
--- a/frontend/src/metabase/hoc/Tooltipify.jsx
+++ b/frontend/src/metabase/hoc/Tooltipify.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Tooltip from "metabase/components/Tooltip";
 
diff --git a/frontend/src/metabase/hoc/Typeahead.jsx b/frontend/src/metabase/hoc/Typeahead.jsx
index 2fa2e872eb7b21a5a3fc1c6401a126d7475c95d6..bbe356554a3f1f7e201288a67e8cf26aa99e3b34 100644
--- a/frontend/src/metabase/hoc/Typeahead.jsx
+++ b/frontend/src/metabase/hoc/Typeahead.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx
index 69dcce269099d519495e83ac6a2ff91787aca92f..fe3a77789457ede273e3b0255c4a84b1e9f83061 100644
--- a/frontend/src/metabase/home/components/Activity.jsx
+++ b/frontend/src/metabase/home/components/Activity.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import _ from 'underscore';
 
@@ -6,7 +7,7 @@ import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper.j
 import ActivityItem from './ActivityItem.jsx';
 import ActivityStory from './ActivityStory.jsx';
 
-import Urls from 'metabase/lib/urls';
+import * as Urls from "metabase/lib/urls";
 
 export default class Activity extends Component {
 
@@ -213,7 +214,7 @@ export default class Activity extends Component {
             case "dashboard-remove-cards":
                 description.body = item.details.dashcards[0].name;
                 if (item.details.dashcards[0].exists) {
-                    description.bodyLink = Urls.card(item.details.dashcards[0].card_id);
+                    description.bodyLink = Urls.question(item.details.dashcards[0].card_id);
                 }
                 break;
             case "metric-create":
diff --git a/frontend/src/metabase/home/components/ActivityItem.jsx b/frontend/src/metabase/home/components/ActivityItem.jsx
index b453b1eb4b8707b228b6bb08c90bb1d67f238621..8d7ba0966acc930656e531126128437b1cfd1792 100644
--- a/frontend/src/metabase/home/components/ActivityItem.jsx
+++ b/frontend/src/metabase/home/components/ActivityItem.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import Icon from 'metabase/components/Icon.jsx';
 import IconBorder from 'metabase/components/IconBorder.jsx';
 import UserAvatar from 'metabase/components/UserAvatar.jsx';
diff --git a/frontend/src/metabase/home/components/ActivityStory.jsx b/frontend/src/metabase/home/components/ActivityStory.jsx
index 93446a019a5984604c536cb5b04c356c696836d2..5c520197a9f9e1c631059ae2e4c6695de42a483f 100644
--- a/frontend/src/metabase/home/components/ActivityStory.jsx
+++ b/frontend/src/metabase/home/components/ActivityStory.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 export default class ActivityStory extends Component {
diff --git a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
index 1de3e68b33880d05c0de16500768e7c36579329b..12e9b1c812fda901b8595a2b0df77ce893a4150f 100644
--- a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
+++ b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
@@ -1,7 +1,9 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import MetabaseSettings from "metabase/lib/settings";
+import * as Urls from "metabase/lib/urls";
 
 export default class NewUserOnboardingModal extends Component {
     constructor(props, context) {
@@ -84,7 +86,7 @@ export default class NewUserOnboardingModal extends Component {
                             {this.renderStep()}
                             <span className="flex-align-right">
                                 <a className="text-underline-hover cursor-pointer mr3" onClick={() => (this.closeModal())}>skip for now</a>
-                                <Link to="/q#?tutorial" className="Button Button--primary">Let's do it!</Link>
+                                <Link to={Urls.question(null, "?tutorial")} className="Button Button--primary">Let's do it!</Link>
                             </span>
                         </div>
                     </div>
diff --git a/frontend/src/metabase/home/components/NextStep.jsx b/frontend/src/metabase/home/components/NextStep.jsx
index a3d1ef4a52fb6f8e118000b5b16e2db8f3f2c726..95424080c7806ae205ac52eedfcc2c450582d26b 100644
--- a/frontend/src/metabase/home/components/NextStep.jsx
+++ b/frontend/src/metabase/home/components/NextStep.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 import fetch from 'isomorphic-fetch';
 
diff --git a/frontend/src/metabase/home/components/RecentViews.jsx b/frontend/src/metabase/home/components/RecentViews.jsx
index f1146966a807b999db69613662d91df7f7d40d4f..a1b0a36ed9e6dc4c3725d04a0e7a98f7493fffce 100644
--- a/frontend/src/metabase/home/components/RecentViews.jsx
+++ b/frontend/src/metabase/home/components/RecentViews.jsx
@@ -1,9 +1,10 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import Icon from "metabase/components/Icon.jsx";
 import SidebarSection from "./SidebarSection.jsx";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { normal } from "metabase/lib/colors";
 
diff --git a/frontend/src/metabase/home/components/SidebarSection.jsx b/frontend/src/metabase/home/components/SidebarSection.jsx
index a83469d754b758be168f3cb03b5f7df2560471cb..cfbd4537bf6a9a9e1473e5c7eb08418f0addc884 100644
--- a/frontend/src/metabase/home/components/SidebarSection.jsx
+++ b/frontend/src/metabase/home/components/SidebarSection.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/home/containers/HomepageApp.jsx b/frontend/src/metabase/home/containers/HomepageApp.jsx
index 1644539b7c056abbebd2c43b7a06f0a82f7fe275..cd5be9797e6a557cfd5b8b7b29a07a2df8e6a9fa 100644
--- a/frontend/src/metabase/home/containers/HomepageApp.jsx
+++ b/frontend/src/metabase/home/containers/HomepageApp.jsx
@@ -1,5 +1,6 @@
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import Greeting from "metabase/lib/greeting";
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index a4c0902142b699967314a7f7bd6e42040f8f51b0..d2686f2f7fa438d72e6e382b4d06198c4959804d 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -213,6 +213,9 @@ ICON_PATHS["horizontal_bar"] = {
     }
 };
 
+// $FlowFixMe
+ICON_PATHS["scalar"] = ICON_PATHS["number"];
+
 export function loadIcon(name) {
     var def = ICON_PATHS[name];
     if (!def) {
diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js
index b53d09d1732ae751d2dcddc8f34f73d8986ac573..2b0ac9090b755dc86b2a981f6fdd1cb00de4faa3 100644
--- a/frontend/src/metabase/lib/card.js
+++ b/frontend/src/metabase/lib/card.js
@@ -1,6 +1,7 @@
 import _ from "underscore";
 import Query, { createQuery } from "metabase/lib/query";
 import Utils from "metabase/lib/utils";
+import * as Urls from "metabase/lib/urls";
 
 import { CardApi } from "metabase/services";
 
@@ -24,10 +25,10 @@ export function startNewCard(type, databaseId, tableId) {
 }
 
 // load a card either by ID or from a base64 serialization.  if both are present then they are merged, which the serialized version taking precedence
+// TODO: move to redux
 export async function loadCard(cardId) {
     try {
-        let card = await CardApi.get({ "cardId": cardId });
-        return card && cleanCopyCard(card);
+        return await CardApi.get({ "cardId": cardId });
     } catch (error) {
         console.log("error loading card", error);
         throw error;
@@ -111,16 +112,10 @@ export function b64url_to_utf8(b64url) {
 }
 
 export function urlForCardState(state, dirty) {
-    var url;
-    if (state.cardId) {
-        url = "/card/" + state.cardId;
-    } else {
-        url = "/q";
-    }
-    if (state.serializedCard && dirty) {
-        url += "#" + state.serializedCard;
-    }
-    return url;
+    return Urls.question(
+        state.cardId,
+        (state.serializedCard && dirty) ? state.serializedCard : ""
+    );
 }
 
 export function cleanCopyCard(card) {
diff --git a/frontend/src/metabase/lib/dataset.js b/frontend/src/metabase/lib/dataset.js
new file mode 100644
index 0000000000000000000000000000000000000000..c5e56bf62088ae03f3cf17a4f883669ad49a9fef
--- /dev/null
+++ b/frontend/src/metabase/lib/dataset.js
@@ -0,0 +1,4 @@
+import _ from "underscore";
+
+// Many aggregations result in [[null]] if there are no rows to aggregate after filters
+export const datasetContainsNoResults = (data) => data.rows.length === 0 || _.isEqual(data.rows, [[null]])
diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js
index ed11450f3b0fd4c68e1de077a1e29dc25ee491ea..7c2eba73166aeef3535ac9f4ee9e9f3b995fba8d 100644
--- a/frontend/src/metabase/lib/urls.js
+++ b/frontend/src/metabase/lib/urls.js
@@ -1,66 +1,71 @@
 import { serializeCardForUrl } from "metabase/lib/card";
 
 // provides functions for building urls to things we care about
-var Urls = {
-    q: function(card) {
-        return "/q#" + serializeCardForUrl(card);
-    },
 
-    card: function(card_id) {
-        // NOTE that this is for an ephemeral card link, not an editable card
-        return "/card/"+card_id;
-    },
-
-    dashboard: function(dashboard_id) {
-        return "/dash/"+dashboard_id;
-    },
+export function question(cardId, cardOrHash = "") {
+    if (cardOrHash && typeof cardOrHash === "object") {
+        cardOrHash = serializeCardForUrl(cardOrHash);
+    }
+    if (cardOrHash && cardOrHash.charAt(0) !== "#") {
+        cardOrHash = "#" + cardOrHash;
+    }
+    // NOTE that this is for an ephemeral card link, not an editable card
+    return cardId != null
+        ? `/question/${cardId}${cardOrHash}`
+        : `/question${cardOrHash}`;
+}
 
-    modelToUrl: function(model, model_id) {
-        switch (model) {
-            case "card":      return Urls.card(model_id);
-            case "dashboard": return Urls.dashboard(model_id);
-            case "pulse":     return Urls.pulse(model_id);
-            default:          return null;
-        }
-    },
+export function dashboard(dashboardId) {
+    return `/dashboard/${dashboardId}`;
+}
 
-    pulse: function(pulse_id) {
-        return "/pulse/#"+pulse_id;
-    },
+export function modelToUrl(model, modelId) {
+    switch (model) {
+        case "card":
+            return question(modelId);
+        case "dashboard":
+            return dashboard(modelId);
+        case "pulse":
+            return pulse(modelId);
+        default:
+            return null;
+    }
+}
 
-    tableRowsQuery: function(database_id, table_id, metric_id, segment_id) {
-        let url = "/q#?db="+database_id+"&table="+table_id;
+export function pulse(pulseId) {
+    return `/pulse/#${pulseId}`;
+}
 
-        if (metric_id) {
-            url = url + "&metric="+metric_id;
-        }
+export function tableRowsQuery(databaseId, tableId, metricId, segmentId) {
+    let query = `?db=${databaseId}&table=${tableId}`;
 
-        if (segment_id) {
-            url = url + "&segment="+segment_id;
-        }
+    if (metricId) {
+        query += `&metric=${metricId}`;
+    }
 
-        return url;
-    },
+    if (segmentId) {
+        query += `&segment=${segmentId}`;
+    }
 
-    collection(collection) {
-        return `/questions/collections/${encodeURIComponent(collection.slug)}`;
-    },
+    return question(null, query);
+}
 
-    label(label) {
-        return `/questions/search?label=${encodeURIComponent(label.slug)}`;
-    },
+export function collection(collection) {
+    return `/questions/collections/${encodeURIComponent(collection.slug)}`;
+}
 
-    publicCard(uuid, type = null) {
-        return `/public/question/${uuid}` + (type ? `.${type}` : ``);
-    },
+export function label(label) {
+    return `/questions/search?label=${encodeURIComponent(label.slug)}`;
+}
 
-    publicDashboard(uuid) {
-        return `/public/dashboard/${uuid}`;
-    },
+export function publicCard(uuid, type = null) {
+    return `/public/question/${uuid}` + (type ? `.${type}` : ``);
+}
 
-    embedCard(token, type = null) {
-        return `/embed/question/${token}` + (type ? `.${type}` : ``);
-    },
+export function publicDashboard(uuid) {
+    return `/public/dashboard/${uuid}`;
 }
 
-export default Urls;
+export function embedCard(token, type = null) {
+    return `/embed/question/${token}` + (type ? `.${type}` : ``);
+}
diff --git a/frontend/src/metabase/nav/components/ProfileLink.jsx b/frontend/src/metabase/nav/components/ProfileLink.jsx
index a244dc74eeef0f54d59301c51cc099b9a1f0bac3..d9b3ad48a7087e8c4656980fdad314394150a644 100644
--- a/frontend/src/metabase/nav/components/ProfileLink.jsx
+++ b/frontend/src/metabase/nav/components/ProfileLink.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper';
diff --git a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
index b902a25096e2c25ff4cdac9d6854c6b69643a88c..a4f1cd7c1e93f45ab2f5fdcde2a7838cda786f0f 100644
--- a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
+++ b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
@@ -1,14 +1,16 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { Link } from "react-router";
 
 import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper.jsx';
-
-import MetabaseAnalytics from "metabase/lib/analytics";
 import CreateDashboardModal from "metabase/components/CreateDashboardModal.jsx";
 import Modal from "metabase/components/Modal.jsx";
 import ConstrainToScreen from "metabase/components/ConstrainToScreen";
 
+import MetabaseAnalytics from "metabase/lib/analytics";
+import * as Urls from "metabase/lib/urls";
+
 import _ from "underscore";
 import cx from "classnames";
 
@@ -62,7 +64,7 @@ export default class DashboardsDropdown extends Component {
         try {
             let action = await createDashboard(newDashboard, true);
             // FIXME: this doesn't feel right...
-            this.props.onChangeLocation(`/dash/${action.payload.id}`);
+            this.props.onChangeLocation(Urls.dashboard(action.payload.id));
         } catch (e) {
             console.log("createDashboard failed", e);
         }
@@ -137,7 +139,7 @@ export default class DashboardsDropdown extends Component {
                                     <ul className="NavDropdown-content-layer">
                                         { dashboards.map(dash =>
                                             <li key={dash.id} className="block">
-                                                <Link to={"/dash/"+dash.id} data-metabase-event={"Navbar;Dashboard Dropdown;Open Dashboard;"+dash.id} className="Dropdown-item block text-white no-decoration" onClick={this.closeDropdown}>
+                                                <Link to={Urls.dashboard(dash.id)} data-metabase-event={"Navbar;Dashboard Dropdown;Open Dashboard;"+dash.id} className="Dropdown-item block text-white no-decoration" onClick={this.closeDropdown}>
                                                     <div className="flex text-bold">
                                                         {dash.name}
                                                     </div>
diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx
index 74520d03ff4d49a44a9bc1a91e2df4f3bf0ee24c..3c286bcbfaa7606cafcb486063df0cab68c61d68 100644
--- a/frontend/src/metabase/nav/containers/Navbar.jsx
+++ b/frontend/src/metabase/nav/containers/Navbar.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 import { connect } from "react-redux";
@@ -11,6 +12,8 @@ import LogoIcon from "metabase/components/LogoIcon.jsx";
 import DashboardsDropdown from "metabase/nav/containers/DashboardsDropdown.jsx";
 import ProfileLink from "metabase/nav/components/ProfileLink.jsx";
 
+import * as Urls from "metabase/lib/urls";
+
 import { getPath, getContext, getUser } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
@@ -115,7 +118,13 @@ export default class Navbar extends Component {
                     </li>
                     <li className="pl3">
                         <DashboardsDropdown {...this.props}>
-                            <a data-metabase-event={"Navbar;Dashboard Dropdown;Toggle"} style={this.styles.navButton} className={cx("NavDropdown-button NavItem text-white text-bold cursor-pointer px2 flex align-center transition-background", {"NavItem--selected": this.isActive("/dash/")})}>
+                            <a
+                                data-metabase-event={"Navbar;Dashboard Dropdown;Toggle"}
+                                style={this.styles.navButton}
+                                className={cx("NavDropdown-button NavItem text-white text-bold cursor-pointer px2 flex align-center transition-background", {
+                                    "NavItem--selected": this.isActive("/dashboard/")
+                                })}
+                            >
                                 <span className="NavDropdown-button-layer">
                                     Dashboards
                                     <Icon className="ml1" name={'chevrondown'} size={8}></Icon>
@@ -133,7 +142,7 @@ export default class Navbar extends Component {
                         <Link to="/reference/guide" data-metabase-event={"Navbar;DataReference"} style={this.styles.navButton} className={cx("NavItem cursor-pointer text-white text-bold no-decoration flex align-center px2 transition-background")} activeClassName="NavItem--selected">Data Reference</Link>
                     </li>
                     <li className="pl3">
-                        <Link to="/q" data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">New <span className="hide sm-show">Question</span></Link>
+                        <Link to={Urls.question()} data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">New <span className="hide sm-show">Question</span></Link>
                     </li>
                     <li className="flex-align-right transition-background">
                         <div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div>
diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx
index 531adf83a66cf13a2693a7b469403048ab198ca0..cc4b7fc0512e8ffc6aaeda9ab2078e94970044b2 100644
--- a/frontend/src/metabase/public/components/EmbedFrame.jsx
+++ b/frontend/src/metabase/public/components/EmbedFrame.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { withRouter } from "react-router"; import { IFRAMED } from "metabase/lib/dom";
 
 import Parameters from "metabase/dashboard/containers/Parameters";
diff --git a/frontend/src/metabase/public/components/MetabaseEmbed.jsx b/frontend/src/metabase/public/components/MetabaseEmbed.jsx
index 841d3fa3f257ccfe8df84a3e18189c58de81b8f4..80635a96ca845345af476a2c217bd796ba424517 100644
--- a/frontend/src/metabase/public/components/MetabaseEmbed.jsx
+++ b/frontend/src/metabase/public/components/MetabaseEmbed.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import querystring from "querystring";
 import _ from "underscore";
diff --git a/frontend/src/metabase/public/components/PublicError.jsx b/frontend/src/metabase/public/components/PublicError.jsx
index 01e429cfccb78ec6abee1f68eb53a84e6eb06c87..d0ce8d1dffd6c61af60674731ff13ea48793e439 100644
--- a/frontend/src/metabase/public/components/PublicError.jsx
+++ b/frontend/src/metabase/public/components/PublicError.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { connect } from "react-redux";
 import { getErrorMessage } from "metabase/selectors/app";
diff --git a/frontend/src/metabase/public/components/PublicNotFound.jsx b/frontend/src/metabase/public/components/PublicNotFound.jsx
index 1c775ff4a478a514c52d88d3af66ac0171215db0..0ae0e771ad8a1a2bfb0e7414ce6a0cebd3c534ac 100644
--- a/frontend/src/metabase/public/components/PublicNotFound.jsx
+++ b/frontend/src/metabase/public/components/PublicNotFound.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import EmbedFrame from "./EmbedFrame";
 
diff --git a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
index 1462d528d9c0157b4dce35cbebee57c654ef7198..f5f4d111e1c43a7e643ebcb2035e79ebb0b7e91b 100644
--- a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import ToggleLarge from "metabase/components/ToggleLarge";
 import Button from "metabase/components/Button";
diff --git a/frontend/src/metabase/public/components/widgets/CodeSample.jsx b/frontend/src/metabase/public/components/widgets/CodeSample.jsx
index 0db42c7e1b0280b3e535739603be425ddf7665f5..f236fd875b1a1c91c9f6fa38f28bda6e6aa0e217 100644
--- a/frontend/src/metabase/public/components/widgets/CodeSample.jsx
+++ b/frontend/src/metabase/public/components/widgets/CodeSample.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Select, { Option } from "metabase/components/Select";
 import CopyButton from "metabase/components/CopyButton";
diff --git a/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx b/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx
index 57aa1fdca1d39a54a0e0509f2320be1c86ff20f6..748840682947e4497e769589277ecbda7ca447de 100644
--- a/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import EmbedSelect from "./EmbedSelect";
 import CheckBox from "metabase/components/CheckBox";
diff --git a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx
index 5671264be8f8465f13e0f5c600c0bbdd27275f43..3958f551e00d797aed0d33242f430fbd199dcc3e 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import ExternalLink from "metabase/components/ExternalLink";
 import CodeSample from "./CodeSample";
diff --git a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
index d5796ef95c8f5894a472e77a841f311290e54c6b..e46b9fb677fc6188dca638e23fd76bf77498dd4d 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { titleize } from "inflection";
 
diff --git a/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx b/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx
index 3ef3b3d0867ad1b54db2019d6b5c5f6b8cd6d0ea..5e569c9b2859318c112f50fe8922d316f6149246 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Icon from "metabase/components/Icon";
 
diff --git a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
index a890aedafeb465b284b43564ff4fb37dc801a709..05a759a4ba821eb7b8f3f1e96513bd805879ccfb 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import Tooltip from "metabase/components/Tooltip";
@@ -29,7 +29,7 @@ export default class EmbedWidget extends Component<*, Props, *> {
                 ref={m => this._modal = m}
                 full
                 triggerElement={
-                    <Tooltip tooltip={`Sharing and Embedding`}>
+                    <Tooltip tooltip={`Sharing and embedding`}>
                         <Icon name="share" onClick={() => MetabaseAnalytics.trackEvent("Sharing / Embedding", resourceType, "Sharing Link Clicked") } />
                     </Tooltip>
                 }
diff --git a/frontend/src/metabase/public/components/widgets/PreviewPane.jsx b/frontend/src/metabase/public/components/widgets/PreviewPane.jsx
index 975f527b869fe77bd6d2a18e9281f311a239540b..28fa3d17766654dbf287d70d37febbf6eff3f4f7 100644
--- a/frontend/src/metabase/public/components/widgets/PreviewPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/PreviewPane.jsx
@@ -1,5 +1,5 @@
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
index 624896fa8151cea1102f8fa283eb314a0f5bd788..6d9a8aa8ad4775775f04cd79cd4301ba855c0150 100644
--- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import RetinaImage from "react-retina-image";
 import Icon from "metabase/components/Icon";
diff --git a/frontend/src/metabase/public/containers/PublicApp.jsx b/frontend/src/metabase/public/containers/PublicApp.jsx
index 8155831aa9872f72309f2931d8194831d8ce74e3..9405c6015ae048f78ea2c1ade05614d4034f5761 100644
--- a/frontend/src/metabase/public/containers/PublicApp.jsx
+++ b/frontend/src/metabase/public/containers/PublicApp.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import PublicNotFound from "metabase/public/components/PublicNotFound";
diff --git a/frontend/src/metabase/public/containers/PublicDashboard.jsx b/frontend/src/metabase/public/containers/PublicDashboard.jsx
index c7c83caeee2fbc3bf1abc303f519c2a8fddb4e6b..8e4e688aa1fd990217ac551530c536d83e945fba 100644
--- a/frontend/src/metabase/public/containers/PublicDashboard.jsx
+++ b/frontend/src/metabase/public/containers/PublicDashboard.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
diff --git a/frontend/src/metabase/public/containers/PublicQuestion.jsx b/frontend/src/metabase/public/containers/PublicQuestion.jsx
index cccf1b1cbc96ce0ba40574c4276410182829edba..cdbba6e144db90b25de5ea9b6502939acc18f19b 100644
--- a/frontend/src/metabase/public/containers/PublicQuestion.jsx
+++ b/frontend/src/metabase/public/containers/PublicQuestion.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import Visualization from "metabase/visualizations/components/Visualization";
diff --git a/frontend/src/metabase/pulse/components/CardPicker.jsx b/frontend/src/metabase/pulse/components/CardPicker.jsx
index 9a0069980853c17f5fe10fbcd283f4eea6af0f9d..7d5d883862ed863853644fa679a9ca4893824539 100644
--- a/frontend/src/metabase/pulse/components/CardPicker.jsx
+++ b/frontend/src/metabase/pulse/components/CardPicker.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
index 2afafd3dbf8116adf9fdcca8acbcb85f3a41f56e..fd6a1aac9ac37b26fd8d5e9013bbaa62fab54204 100644
--- a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
+++ b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 /*eslint-disable react/no-danger */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx
index 7d50c9bae6f5d01a32bb07201cf5dd07a071fc45..a212847eebcf3b8f43d9e1ca9c25245d5212b4cb 100644
--- a/frontend/src/metabase/pulse/components/PulseEdit.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import PulseEditName from "./PulseEditName.jsx";
diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
index 2f631f9459aefdf43028074c2b58252b0a4ec5a1..b05bd4e20befa1c9e058b77094bf179a6a40425b 100644
--- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import CardPicker from "./CardPicker.jsx";
 import PulseCardPreview from "./PulseCardPreview.jsx";
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index c92a3215312688b996aea6152064d99f87ea7cc5..56363d46a080273da1a2387371a4a5c073569ebc 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import _ from "underscore";
 import { assoc, assocIn } from "icepick";
diff --git a/frontend/src/metabase/pulse/components/PulseEditName.jsx b/frontend/src/metabase/pulse/components/PulseEditName.jsx
index c3c0cbacecf4af47b203cea549a8df20981d0c98..f725b04685b775ec4f8868ef305f9c86c6bc45cb 100644
--- a/frontend/src/metabase/pulse/components/PulseEditName.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditName.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import _ from "underscore";
diff --git a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
index 4c17054314ebac9831fd5b0dfc43c0f5a5b6ac10..21b99a6cd4578a4486325be134be7991cd65d17d 100644
--- a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Toggle from "metabase/components/Toggle.jsx";
 
diff --git a/frontend/src/metabase/pulse/components/PulseList.jsx b/frontend/src/metabase/pulse/components/PulseList.jsx
index 4df6d170d0f5ecbb4d599f26a507957f93488eb8..e560516d847ae2e1a43acedc46b8d41f7736b698 100644
--- a/frontend/src/metabase/pulse/components/PulseList.jsx
+++ b/frontend/src/metabase/pulse/components/PulseList.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import PulseListItem from "./PulseListItem.jsx";
 import WhatsAPulse from "./WhatsAPulse.jsx";
diff --git a/frontend/src/metabase/pulse/components/PulseListChannel.jsx b/frontend/src/metabase/pulse/components/PulseListChannel.jsx
index 3c53647b0540b7cfaddd5df7274f9fb34cb160cd..e7775c1bc330d5df1c9ec22295e2121d10bae5b0 100644
--- a/frontend/src/metabase/pulse/components/PulseListChannel.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListChannel.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx
index 7429928e757872a2c685fb3d3549fe073191833a..116ab53f43942d34a8824f12755f3d95cd4d2ce7 100644
--- a/frontend/src/metabase/pulse/components/PulseListItem.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx
@@ -1,11 +1,12 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { Link } from "react-router";
 
 import cx from "classnames";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 import PulseListChannel from "./PulseListChannel.jsx";
 
 export default class PulseListItem extends Component {
@@ -43,7 +44,7 @@ export default class PulseListItem extends Component {
                 <ol className="mb2 px4 flex flex-wrap">
                     { pulse.cards.map((card, index) =>
                         <li key={index} className="mr1 mb1">
-                            <Link to={Urls.card(card.id)} className="Button">
+                            <Link to={Urls.question(card.id)} className="Button">
                                 {card.name}
                             </Link>
                         </li>
diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
index 67db8b2cd3cca1a4e5706a7d378f8ce417933624..b53fba16aa3e68fbe5efedfbf9615c4a867a2405 100644
--- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx
+++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/pulse/components/SchedulePicker.jsx b/frontend/src/metabase/pulse/components/SchedulePicker.jsx
index 99e70181c37960d4ed5a2a5839b88653f4bcd024..69ed785abcc22b32d6be6e61fcdc2ed50fbbf1f5 100644
--- a/frontend/src/metabase/pulse/components/SchedulePicker.jsx
+++ b/frontend/src/metabase/pulse/components/SchedulePicker.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Select from "metabase/components/Select.jsx";
 
diff --git a/frontend/src/metabase/pulse/components/SetupMessage.jsx b/frontend/src/metabase/pulse/components/SetupMessage.jsx
index 1efa3a30e05bfc5e982548cd0289ae35ed46d0bf..416321fdfc6459670743075f335b6d0320e7c847 100644
--- a/frontend/src/metabase/pulse/components/SetupMessage.jsx
+++ b/frontend/src/metabase/pulse/components/SetupMessage.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import Settings from "metabase/lib/settings";
diff --git a/frontend/src/metabase/pulse/components/SetupModal.jsx b/frontend/src/metabase/pulse/components/SetupModal.jsx
index e4996fa8e276b8177dfb9a138019174b9850eaba..c0a7edbf93a14aa7291ec26b38ed63968979d008 100644
--- a/frontend/src/metabase/pulse/components/SetupModal.jsx
+++ b/frontend/src/metabase/pulse/components/SetupModal.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import SetupMessage from "./SetupMessage.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
diff --git a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
index 639fc20d4d31ba45b8af9786078a60a81f95157a..0d44604a99758b1ed9ba13be70b6a39cde1619f4 100644
--- a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
+++ b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import RetinaImage from "react-retina-image";
 
diff --git a/frontend/src/metabase/pulse/containers/PulseEditApp.jsx b/frontend/src/metabase/pulse/containers/PulseEditApp.jsx
index fc7510993e1fe53817dcceb2f9abea3a30e77964..60c68b4aa41af0ee53acb4adb1de150d9ef6a8b8 100644
--- a/frontend/src/metabase/pulse/containers/PulseEditApp.jsx
+++ b/frontend/src/metabase/pulse/containers/PulseEditApp.jsx
@@ -1,5 +1,5 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
diff --git a/frontend/src/metabase/pulse/containers/PulseListApp.jsx b/frontend/src/metabase/pulse/containers/PulseListApp.jsx
index bc01b350b7c702cf68433deae0bf273124ff7677..239841391e1608daede3a89807d13a8d14806af7 100644
--- a/frontend/src/metabase/pulse/containers/PulseListApp.jsx
+++ b/frontend/src/metabase/pulse/containers/PulseListApp.jsx
@@ -1,5 +1,5 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
diff --git a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
index bafa6394ae0c9402c97694f6ffd4e60f7c25a0f9..285e528400e100284e6c370c8d317912019f1290 100644
--- a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
+++ b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import DatePicker
     from "metabase/query_builder/components/filters/pickers/DatePicker";
@@ -34,7 +34,7 @@ type Props = {
     card: CardObject,
     tableMetadata: TableMetadata,
     setDatasetQuery: (datasetQuery: DatasetQuery) => void,
-    runQueryFn: () => void
+    runQuery: () => void
 };
 
 type State = {
@@ -90,7 +90,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> {
             card,
             tableMetadata,
             setDatasetQuery,
-            runQueryFn
+            runQuery
         } = this.props;
         const { filter, filterIndex, currentFilter } = this.state;
         let currentDescription;
@@ -148,7 +148,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> {
                                     ...card.dataset_query,
                                     query
                                 });
-                                runQueryFn();
+                                runQuery();
                             }
                             if (this._popover) {
                                 this._popover.close();
diff --git a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
index 4681fdbe7a139b647831b85cdeb18b9ec02bcb36..f548331efd29641dc6a6fe19063a0313b71bcd5b 100644
--- a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
+++ b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import TimeGroupingPopover
     from "metabase/query_builder/components/TimeGroupingPopover";
@@ -20,14 +20,14 @@ import type {
 type Props = {
     card: CardObject,
     setDatasetQuery: (datasetQuery: DatasetQuery) => void,
-    runQueryFn: () => void
+    runQuery: () => void
 };
 
 export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
     _popover: ?any;
 
     render() {
-        const { card, setDatasetQuery, runQueryFn } = this.props;
+        const { card, setDatasetQuery, runQuery } = this.props;
         if (Card.isStructured(card)) {
             const query = Card.getQuery(card);
             const breakouts = query && Query.getBreakouts(query);
@@ -62,7 +62,7 @@ export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
                                     ...card.dataset_query,
                                     query
                                 });
-                                runQueryFn();
+                                runQuery();
                                 if (this._popover) {
                                     this._popover.close();
                                 }
diff --git a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
index 1165f67ffc6972bb4a4f4d8658fa29c53b6d2e2b..a7b811801fd68a06dab6d27736107fb536417d21 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React from "react";
-
 import { isCategory, isAddress } from "metabase/lib/schema_metadata";
 
 import PivotByAction from "./PivotByAction";
diff --git a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
index 218ee7f08200ec2cf15853ae6efc9d7bb36b9ddf..0b4f241ba1da0478052bbd065c13e90f81d93751 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React from "react";
-
 import { isAddress } from "metabase/lib/schema_metadata";
 
 import PivotByAction from "./PivotByAction";
diff --git a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
index b80c6bdc549b020ae6bfc10110594c1e0a151966..c70764e3f0468469ee2420b014d57842076c2941 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React from "react";
-
 import { isDate } from "metabase/lib/schema_metadata";
 
 import PivotByAction from "./PivotByAction";
diff --git a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
index 8d05d6502b7926dc22ebae4c412c6d8e6f7d08b9..97a53e6d458dc884197c81465d838a3498496b13 100644
--- a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import AggregationPopover from "metabase/qb/components/gui/AggregationPopover";
 
diff --git a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
index f10a4d8aa883711cd2835519fce48fa98f2feb5b..fdc7580a417f1ff565f6ccb87b3959b616b0e03e 100644
--- a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
-
 import PivotByCategoryAction from "../actions/PivotByCategoryAction";
 
 import type {
diff --git a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
index 385a54969550c2c2a75eb29c69c5d10f7e8872fb..b365e0b955e53f41b3c5b57a09136a8419e3128f 100644
--- a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
-
 import PivotByLocationAction from "../actions/PivotByLocationAction";
 
 import type {
diff --git a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
index 110d5c12336756e9c662a951576c13c3f9636d3c..9d2c7969d1bb443e5540abfbdafab20323422031 100644
--- a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
-
 import PivotByTimeAction from "../actions/PivotByTimeAction";
 
 import type {
diff --git a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
index e5ff6ff997038f31d6aaf371033c6f3f9dceded0..7e5e0bf1b9fd81dd59c7189db6736f7c7cb25191 100644
--- a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
+++ b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import AggPopover from "metabase/query_builder/components/AggregationPopover";
 
diff --git a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
index 285fe299bf3f88eab7707ce725e8c95121da2897..2a985b1f39e39d68b7158d0852102b8bf08b5afb 100644
--- a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
+++ b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import FieldList from "metabase/query_builder/components/FieldList.jsx";
 
diff --git a/frontend/src/metabase/qb/components/modes/NativeMode.jsx b/frontend/src/metabase/qb/components/modes/NativeMode.jsx
index abaf45d3587774a6bc70c4f867973e8868c17a1f..debf0cea4f81a0313d1a391be1afe77d97096249 100644
--- a/frontend/src/metabase/qb/components/modes/NativeMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/NativeMode.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React from "react";
-
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const NativeMode: QueryMode = {
diff --git a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
index 974154f82bb70f8acefd6f529d8b7ceb0f38bca0..c302cc83f025dfa3031cd437b39e38c25b463dd3 100644
--- a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
@@ -1,7 +1,5 @@
 /* @flow */
 
-import React from "react";
-
 import { DEFAULT_ACTIONS } from "../actions";
 import { DEFAULT_DRILLS } from "../drill";
 
diff --git a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
index e60cbf8232ba836cec1afc73637919db6915aa07..8f77ec576b22fa63acda94a7586008ba404321b0 100644
--- a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import TimeseriesGroupingWidget
     from "metabase/qb/components/TimeseriesGroupingWidget";
@@ -29,7 +29,7 @@ type Props = {
     lastRunCard: CardObject,
     tableMetadata: TableMetadata,
     setDatasetQuery: (datasetQuery: DatasetQuery) => void,
-    runQueryFn: () => void
+    runQuery: () => void
 };
 
 export const TimeseriesModeFooter = (props: Props) => {
diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js
index 3916b647c42d54ae30d8c43ceadef1f7f42897b8..4baf7bc67caf8d82235f288e7c414acf955ba8cb 100644
--- a/frontend/src/metabase/query_builder/actions.js
+++ b/frontend/src/metabase/query_builder/actions.js
@@ -223,7 +223,7 @@ export const initializeQB = createThunkAction(INITIALIZE_QB, (location, params)
         if (card && card.dataset_query && (Query.canRun(card.dataset_query.query) || card.dataset_query.type === "native")) {
             // NOTE: timeout to allow Parameters widget to set parameterValues
             setTimeout(() =>
-                dispatch(runQuery(card, false))
+                dispatch(runQuery(card, { shouldUpdateUrl: false }))
             , 0);
         }
 
@@ -285,7 +285,7 @@ export const cancelEditing = createThunkAction(CANCEL_EDITING, () => {
         dispatch(loadMetadataForCard(card));
 
         // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
-        dispatch(runQuery(card, false));
+        dispatch(runQuery(card, { shouldUpdateUrl: false }));
         dispatch(updateUrl(card, { dirty: false }));
 
         MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Cancel");
@@ -466,7 +466,7 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
         dispatch(loadMetadataForCard(card));
 
         // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
-        dispatch(runQuery(card, false));
+        dispatch(runQuery(card, { shouldUpdateUrl: false }));
         dispatch(updateUrl(card, { dirty: false }));
 
         return card;
@@ -482,7 +482,7 @@ export const setCardAndRun = createThunkAction(SET_CARD_AND_RUN, (runCard, shoul
 
         dispatch(loadMetadataForCard(card));
 
-        dispatch(runQuery(card, shouldUpdateUrl));
+        dispatch(runQuery(card, { shouldUpdateUrl: shouldUpdateUrl }));
 
         return card;
     };
@@ -850,7 +850,11 @@ export const removeQueryExpression = createQueryAction(
 
 // runQuery
 export const RUN_QUERY = "metabase/qb/RUN_QUERY";
-export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = true, parameterValues, dirty) => {
+export const runQuery = createThunkAction(RUN_QUERY, (card, {
+    shouldUpdateUrl = true,
+    ignoreCache = false, // currently only implemented for saved cards
+    parameterValues
+} = {}) => {
     return async (dispatch, getState) => {
         const state = getState();
         const parameters = getParameters(state);
@@ -880,8 +884,12 @@ export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = tr
         const datasetQuery = applyParameters(card, parameters, parameterValues);
 
         // use the CardApi.query if the query is saved and not dirty so users with view but not create permissions can see it.
-        if (card.id && !cardIsDirty) {
-            CardApi.query({ cardId: card.id, parameters: datasetQuery.parameters }, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
+        if (card.id != null && !cardIsDirty) {
+            CardApi.query({
+                cardId: card.id,
+                parameters: datasetQuery.parameters,
+                ignore_cache: ignoreCache
+            }, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
         } else {
             MetabaseApi.dataset(datasetQuery, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
         }
@@ -1108,8 +1116,6 @@ export const toggleDataReferenceFn = toggleDataReference;
 export const onBeginEditing = beginEditing;
 export const onCancelEditing = cancelEditing;
 export const setQueryModeFn = setQueryMode;
-export const runQueryFn = runQuery;
-export const cancelQueryFn = cancelQuery;
 export const setDatabaseFn = setQueryDatabase;
 export const setSourceTableFn = setQuerySourceTable;
 export const setDisplayFn = setCardVisualization;
diff --git a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
index fccb165d952d715624ed713e5424272bf87efe36..1cc9e5473c74ad2ea4b657243ef9e581310b6541 100644
--- a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon";
 import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
diff --git a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx
index 27bbc17d244484ad8102908955119be6a202fe9a..fe49f92665908f8f6dcb51ded6dd25725b2aca70 100644
--- a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx
+++ b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import IconBorder from "metabase/components/IconBorder.jsx";
diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
index d02705b9ebcca688e4dcee8dd40e0b7561e9c894..d4e53eef082ac454dba7c90356a38e78a1f48382 100644
--- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import AccordianList from "metabase/components/AccordianList.jsx";
 import FieldList from './FieldList.jsx';
diff --git a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
index d833d2c8d72f55115a1c26c316dfe407b4f4daad..a36a9b8e50a36607ca2b0afd41c0954125fcde88 100644
--- a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import AggregationPopover from "./AggregationPopover.jsx";
 import FieldName from './FieldName.jsx';
diff --git a/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx b/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx
index 4719bfb7cdfa6374797256a4ca0b1c312a7481c5..285e31223afbbc099236b38f918956b36c667998 100644
--- a/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import FieldList from "./FieldList.jsx";
 import FieldName from "./FieldName.jsx";
diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx
index 24504e412ba6229f7f328998edaede93e7f73663..1b8a48983a2c2636121fdced07ad8a41abc90b8d 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
diff --git a/frontend/src/metabase/query_builder/components/ExpandableString.jsx b/frontend/src/metabase/query_builder/components/ExpandableString.jsx
index 90127363e4ba354f4d10e59ee0997bfd852a37a0..002cefdf5cd9ef05943077c7a27ae69fcbdd3568 100644
--- a/frontend/src/metabase/query_builder/components/ExpandableString.jsx
+++ b/frontend/src/metabase/query_builder/components/ExpandableString.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Humanize from 'humanize';
 
diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
index b5c13ee78ad4490114ded991d31ec7818e5ee9d3..c6faaf9dfe4a0911ef2908d475ffb367d471e120 100644
--- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import _ from "underscore";
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/query_builder/components/FieldList.jsx b/frontend/src/metabase/query_builder/components/FieldList.jsx
index 143ad547f31595ec1e518f5f7789ddfdc3c12728..9ecf0e7efea297727035833ee0f7a4cd50a38b78 100644
--- a/frontend/src/metabase/query_builder/components/FieldList.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import AccordianList from "metabase/components/AccordianList.jsx";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/query_builder/components/FieldName.jsx b/frontend/src/metabase/query_builder/components/FieldName.jsx
index d21e0f44cf5f2d4b39df63adcf0f633e3ffef4aa..d5aa48c5d6219ec811d8c9255e2c8482bb73bdac 100644
--- a/frontend/src/metabase/query_builder/components/FieldName.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldName.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import Clearable from "./Clearable.jsx";
diff --git a/frontend/src/metabase/query_builder/components/FieldWidget.jsx b/frontend/src/metabase/query_builder/components/FieldWidget.jsx
index 08232b33d1713c3622af3545349baf599f97716b..659157a747cfe053ecac30fdcd50a974ebe3c09f 100644
--- a/frontend/src/metabase/query_builder/components/FieldWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import FieldList from "./FieldList.jsx";
 import FieldName from "./FieldName.jsx";
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index d6d99f521255d3c2346f567e42fcf0597c5dd8d8..9dee1e629a51b25e64a3275516e9f95a6f222f7d 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import AggregationWidget from './AggregationWidget.jsx';
diff --git a/frontend/src/metabase/query_builder/components/LimitWidget.jsx b/frontend/src/metabase/query_builder/components/LimitWidget.jsx
index ab72989a4eb133649ec466138006b59a55305d04..a85f0069c96e2ef60abe9556295a36dea70c8b21 100644
--- a/frontend/src/metabase/query_builder/components/LimitWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/LimitWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index a68ac9cb3ec9b4be627ff43cf2fda37e950c0809..645519774829e6067f5ae4a151203d72d76bf3da 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -1,7 +1,8 @@
 /*global ace*/
 /* eslint "react/prop-types": "warn" */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import "./NativeQueryEditor.css";
@@ -90,7 +91,7 @@ export default class NativeQueryEditor extends Component {
         nativeDatabases: PropTypes.array.isRequired,
         datasetQuery: PropTypes.object.isRequired,
         setDatasetQuery: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
         setDatabaseFn: PropTypes.func.isRequired,
         autocompleteResultsFn: PropTypes.func.isRequired,
         isOpen: PropTypes.bool,
@@ -163,10 +164,10 @@ export default class NativeQueryEditor extends Component {
                 const selectedText = this._editor.getSelectedText();
                 if (selectedText) {
                     const temporaryCard = assocIn(card, ["dataset_query", "native", "query"], selectedText);
-                    this.props.runQueryFn(temporaryCard, false, null, true);
+                    this.props.runQuery(temporaryCard, { shouldUpdateUrl: false });
                 }
             } else {
-                this.props.runQueryFn();
+                this.props.runQuery();
             }
         }
     }
diff --git a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
index d86211846e3dc53b74ec7653ade9deee383bb5fd..d7fa933a7dfee7e152f3e9c82340cab0264f0329 100644
--- a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import FilterList from "./filters/FilterList.jsx";
 import AggregationWidget from "./AggregationWidget.jsx";
diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
index 58ead4423d3d5bfc1e7931c0bb4b6f11bb1d2664..0726bbdc7a7ab439a2c3ae241e583881b85743a9 100644
--- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import Icon from "metabase/components/Icon.jsx";
@@ -7,7 +8,7 @@ import Tooltip from "metabase/components/Tooltip.jsx";
 
 import FieldSet from "metabase/components/FieldSet.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import _ from "underscore";
 import cx from "classnames";
diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
index 2e61da66556b74a6600bced942a4b3457a78ee0d..046d39df20344c0c18fe931d4b1d872140ad83ca 100644
--- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import QueryModeButton from "./QueryModeButton.jsx";
@@ -23,7 +24,7 @@ import { CardApi, RevisionApi } from "metabase/services";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import Query from "metabase/lib/query";
 import { cancelable } from "metabase/lib/promise";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 import _ from "underscore";
diff --git a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
index 6e0fd438dc233057f7d54f366a3254da773b9b47..47f0890a29fe8f2927f0665df26a90e63c5f80c5 100644
--- a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 import { formatSQL, capitalize } from "metabase/lib/formatting";
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index c236ebd05c6a7eca95310cfbf2ae8f178ae4a85a..5cdc602c9e48f5dcd5576eb669e6c44c0b2c5a93 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -1,7 +1,12 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx';
+import Tooltip from "metabase/components/Tooltip";
+import Icon from "metabase/components/Icon";
+import ShrinkableList from "metabase/components/ShrinkableList";
+
 import RunButton from './RunButton.jsx';
 import VisualizationSettings from './VisualizationSettings.jsx';
 
@@ -12,12 +17,16 @@ import Warnings from "./Warnings.jsx";
 import QueryDownloadWidget from "./QueryDownloadWidget.jsx";
 import QuestionEmbedWidget from "../containers/QuestionEmbedWidget";
 
-import { formatNumber, inflect } from "metabase/lib/formatting";
+import { formatNumber, inflect, duration } from "metabase/lib/formatting";
 import Utils from "metabase/lib/utils";
 import MetabaseSettings from "metabase/lib/settings";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 import _ from "underscore";
+import moment from "moment";
+
+const REFRESH_TOOLTIP_THRESHOLD = 30 * 1000; // 30 seconds
 
 export default class QueryVisualization extends Component {
     constructor(props, context) {
@@ -39,8 +48,8 @@ export default class QueryVisualization extends Component {
         cellClickedFn: PropTypes.func,
         isRunning: PropTypes.bool.isRequired,
         isRunnable: PropTypes.bool.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
-        cancelQueryFn: PropTypes.func
+        runQuery: PropTypes.func.isRequired,
+        cancelQuery: PropTypes.func
     };
 
     static defaultProps = {
@@ -62,44 +71,85 @@ export default class QueryVisualization extends Component {
         }
     }
 
-    queryIsDirty() {
-        // a query is considered dirty if ANY part of it has been changed
-        return (
-            !Utils.equals(this.props.card.dataset_query, this.state.lastRunDatasetQuery) ||
-            !Utils.equals(this.props.parameterValues, this.state.lastRunParameterValues)
-        );
-    }
-
     isChartDisplay(display) {
         return (display !== "table" && display !== "scalar");
     }
 
+    runQuery = () => {
+        this.props.runQuery(null, { ignoreCache: true });
+    }
+
     renderHeader() {
-        const { isObjectDetail, isRunnable, isRunning, isAdmin, card, result, runQueryFn, cancelQueryFn } = this.props;
-        const isDirty = this.queryIsDirty();
+        const { isObjectDetail, isRunnable, isRunning, isResultDirty, isAdmin, card, result, cancelQuery } = this.props;
         const isSaved = card.id != null;
+
+        let runButtonTooltip;
+        if (!isResultDirty && result && result.cached && result.average_execution_time > REFRESH_TOOLTIP_THRESHOLD) {
+            runButtonTooltip = `This question will take approximately ${duration(result.average_execution_time)} to refresh`;
+        }
+
+        const messages = [];
+        if (result && result.cached) {
+            messages.push({
+                icon: "clock",
+                message: (
+                    <div>
+                        Updated {moment(result.updated_at).fromNow()}
+                    </div>
+                )
+            })
+        }
+        if (result && result.data && !isObjectDetail && card.display === "table") {
+            messages.push({
+                icon: "table2",
+                message: (
+                    <div>
+                        { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")}
+                        <strong>{formatNumber(result.row_count)}</strong>
+                        { " " + inflect("row", result.data.rows.length) }
+                    </div>
+                )
+            })
+        }
+
         const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
         const isEmbeddingEnabled = MetabaseSettings.get("embedding");
         return (
-            <div className="relative flex flex-no-shrink mt3 mb1" style={{ minHeight: "2em" }}>
-                <span className="relative z4">
+            <div className="relative flex align-center flex-no-shrink mt2 mb1" style={{ minHeight: "2em" }}>
+                <div className="z4 flex-full">
                   { !isObjectDetail && <VisualizationSettings ref="settings" {...this.props} /> }
-                </span>
-                <div className="absolute flex layout-centered left right z3">
-                    <RunButton
-                        isRunnable={isRunnable}
-                        isDirty={isDirty}
-                        isRunning={isRunning}
-                        onRun={runQueryFn}
-                        onCancel={cancelQueryFn}
-                    />
                 </div>
-                <div className="absolute right z4 flex align-center" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}>
-                    { !isDirty && this.renderCount() }
+                <div className="z3">
+                    <Tooltip tooltip={runButtonTooltip}>
+                        <RunButton
+                            isRunnable={isRunnable}
+                            isDirty={isResultDirty}
+                            isRunning={isRunning}
+                            onRun={this.runQuery}
+                            onCancel={cancelQuery}
+                        />
+                    </Tooltip>
+                </div>
+                <div className="z4 flex-full flex align-center justify-end" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}>
+                    <ShrinkableList
+                        className="flex"
+                        items={messages}
+                        renderItem={(item) =>
+                            <div className="flex-no-shrink flex align-center mx2 h5 text-grey-4">
+                                <Icon className="mr1" name={item.icon} size={12} />
+                                {item.message}
+                            </div>
+                        }
+                        renderItemSmall={(item) =>
+                            <Tooltip tooltip={<div className="p1">{item.message}</div>}>
+                                <Icon className="mx1" name={item.icon} size={16} />
+                            </Tooltip>
+                        }
+                    />
                     { !isObjectDetail &&
-                        <Warnings warnings={this.state.warnings} className="mx2" size={18} />
+                        <Warnings warnings={this.state.warnings} className="mx1" size={18} />
                     }
-                    { !isDirty && result && !result.error ?
+                    { !isResultDirty && result && !result.error ?
                         <QueryDownloadWidget
                             className="mx1"
                             card={card}
@@ -120,19 +170,6 @@ export default class QueryVisualization extends Component {
         );
     }
 
-    renderCount() {
-        let { result, isObjectDetail, card } = this.props;
-        if (result && result.data && !isObjectDetail && card.display === "table") {
-            return (
-                <div>
-                    { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")}
-                    <b>{formatNumber(result.row_count)}</b>
-                    { " " + inflect("row", result.data.rows.length) }.
-                </div>
-            );
-        }
-    }
-
     render() {
         const { className, card, databases, isObjectDetail, isRunning, result } = this.props
         let viz;
@@ -188,5 +225,5 @@ export default class QueryVisualization extends Component {
 const VisualizationEmptyState = ({showTutorialLink}) =>
     <div className="flex full layout-centered text-grey-1 flex-column">
         <h1>If you give me some data I can show you something cool. Run a Query!</h1>
-        { showTutorialLink && <Link to="/q#?tutorial" className="link cursor-pointer my2">How do I use this thing?</Link> }
+        { showTutorialLink && <Link to={Urls.question(null, "?tutorial")} className="link cursor-pointer my2">How do I use this thing?</Link> }
     </div>
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx b/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx
index df548770d3f4ad88d7c218894ce7787d4f619d4d..de4b791b3fa3fd5a58184e4e1566958f0e08dd02 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ExpandableString from './ExpandableString.jsx';
 import Icon from 'metabase/components/Icon.jsx';
diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx
index 64763834590a41f8a9f0417bb436ffa28ab4793f..91ecceca4a1fa6e6aa8bbe22c18e1ce605246ff7 100644
--- a/frontend/src/metabase/query_builder/components/RunButton.jsx
+++ b/frontend/src/metabase/query_builder/components/RunButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
index c4a08ab92732f8b9166fac9bfbc44c0bd36bf13b..a96780e8789ef7856e65e810d87337ff5cc024b6 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Modal from "metabase/components/Modal.jsx";
 
diff --git a/frontend/src/metabase/query_builder/components/SearchBar.jsx b/frontend/src/metabase/query_builder/components/SearchBar.jsx
index 02467fea4c21df781abc2082ac1298a97798dbc6..abca8cf77a553d7451d85c4abab220305fa03c55 100644
--- a/frontend/src/metabase/query_builder/components/SearchBar.jsx
+++ b/frontend/src/metabase/query_builder/components/SearchBar.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 export default class SearchBar extends React.Component {
diff --git a/frontend/src/metabase/query_builder/components/SelectionModule.jsx b/frontend/src/metabase/query_builder/components/SelectionModule.jsx
index 2bd4a20d9741551b5c557a10c4b307ee32f235ca..3224055dd7097aabaa793cbe254834b892fb1e8a 100644
--- a/frontend/src/metabase/query_builder/components/SelectionModule.jsx
+++ b/frontend/src/metabase/query_builder/components/SelectionModule.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Popover from "metabase/components/Popover.jsx";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/query_builder/components/SortWidget.jsx b/frontend/src/metabase/query_builder/components/SortWidget.jsx
index f499138daa38ccfb725ebcd1412282e5de01081b..dccce59acc8d7911bda53236b306e2127b8c2ca6 100644
--- a/frontend/src/metabase/query_builder/components/SortWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/SortWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import FieldWidget from './FieldWidget.jsx';
diff --git a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
index 029ebb2228be2814afc33f55c52bb78ecf0c7d2e..462c94134b36f94079b7323aa464cdc66a2ecabd 100644
--- a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
 
diff --git a/frontend/src/metabase/query_builder/components/VisualizationError.jsx b/frontend/src/metabase/query_builder/components/VisualizationError.jsx
index a43c2e264b09ef83f4a278b2226703b46fd85030..80b795c90fe77b0a274bedb252da92363cd94ca8 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationError.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationError.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 
 import MetabaseSettings from "metabase/lib/settings";
 import VisualizationErrorMessage from './VisualizationErrorMessage';
diff --git a/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx b/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx
index 0b2908a87dad377141cfc4b4de2a488271b9fdfa..47fb7cdcc6c120e08df7f108b272ec21fc30df60 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { PropTypes } from 'react';
+import React from 'react';
+import PropTypes from "prop-types";
 
 const VisualizationErrorMessage = ({title, type, message, action}) => {
     return (
diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
index 3a6c8a94d96f2c99336e86039b8ba1d8bab8e29a..d959a936de3aa3bd747248fb0e3213abc5c18bb3 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
@@ -1,14 +1,18 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import QueryVisualizationObjectDetailTable from './QueryVisualizationObjectDetailTable.jsx';
 import VisualizationErrorMessage from './VisualizationErrorMessage';
 import Visualization from "metabase/visualizations/components/Visualization.jsx";
+import { datasetContainsNoResults } from "metabase/lib/dataset";
 
 const VisualizationResult = ({card, isObjectDetail, lastRunDatasetQuery, result, ...props}) => {
+    const noResults = datasetContainsNoResults(result.data);
+
     if (isObjectDetail) {
         return <QueryVisualizationObjectDetailTable data={result.data} {...props} />
-    } else if (result.data.rows.length === 0) {
+    } else if (noResults) {
         // successful query but there were 0 rows returned with the result
         return <VisualizationErrorMessage
                   type='noRows'
diff --git a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
index f414f0f5435ca7789ebed3188d4df8a0e11451e4..8e6b07a5f14b754d79a49eec5ed14eb205550d6c 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
diff --git a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
index d706aa202c86dff9be9f120485a698f850a5cc94..13664ab7e8761d31ca1cec679c4c5abed84933c7 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MainPane from './MainPane.jsx';
 import TablePane from './TablePane.jsx';
@@ -26,7 +27,7 @@ export default class DataReference extends Component {
     static propTypes = {
         query: PropTypes.object.isRequired,
         onClose: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
         setDatasetQuery: PropTypes.func.isRequired,
         setDatabaseFn: PropTypes.func.isRequired,
         setSourceTableFn: PropTypes.func.isRequired,
diff --git a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
index f1705a42973259d9435d1f3c21a06bf7c4649b91..cd8203745c1096b91d0e0537ad6453e916822f29 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
index 4cdde08fdb9cbf36e103bc8b713dd75058dea42d..baa074fe261acdd2b819467a73414f92baa51f66 100644
--- a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
@@ -28,7 +29,7 @@ export default class FieldPane extends Component {
         field: PropTypes.object.isRequired,
         datasetQuery: PropTypes.object,
         loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
         setDatasetQuery: PropTypes.func.isRequired,
         setCardAndRun: PropTypes.func.isRequired
     };
@@ -63,7 +64,7 @@ export default class FieldPane extends Component {
         }
         Query.addBreakout(datasetQuery.query, this.props.field.id);
         this.props.setDatasetQuery(datasetQuery);
-        this.props.runQueryFn();
+        this.props.runQuery();
     }
 
     newCard() {
diff --git a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
index 29841da129e10a1a8166b5e8e994e8ce97f2bfb2..7963e969fe1f3420b53b59ea02643890de9c43dd 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import { isQueryable } from "metabase/lib/table";
 
diff --git a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
index 9c64ed132d6340e1da8a613f2c39be54e49a4a33..527782119e3268005aaa5cee37038a7687852823 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
@@ -26,7 +27,7 @@ export default class MetricPane extends Component {
         metric: PropTypes.object.isRequired,
         query: PropTypes.object,
         loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
         setDatasetQuery: PropTypes.func.isRequired,
         setCardAndRun: PropTypes.func.isRequired
     };
diff --git a/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx b/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx
index 7b2c49b6f0953929249a211098636a7878f1768c..3cc284aa23362feba9ac66eeb4f56a16c261c9af 100644
--- a/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import FilterList from "../filters/FilterList.jsx";
 import AggregationWidget from "../AggregationWidget.jsx";
diff --git a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
index 665f1a04008872e4f2ffa428bf4d7de22d686ba6..59878068c3cb032152626501c37b743ed25525f4 100644
--- a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
@@ -27,7 +28,7 @@ export default class SegmentPane extends Component {
         segment: PropTypes.object.isRequired,
         datasetQuery: PropTypes.object,
         loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
         setDatasetQuery: PropTypes.func.isRequired,
         setCardAndRun: PropTypes.func.isRequired
     };
@@ -53,7 +54,7 @@ export default class SegmentPane extends Component {
         }
         Query.addFilter(datasetQuery.query, ["SEGMENT", this.props.segment.id]);
         this.props.setDatasetQuery(datasetQuery);
-        this.props.runQueryFn();
+        this.props.runQuery();
     }
 
     newCard() {
diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
index 99ff5c7e1072fb8619bb814c9ad424383a8cdc9e..206a720240a4325a4fa0aafe00b1bd9ec2860d57 100644
--- a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import QueryButton from "metabase/components/QueryButton.jsx";
 import { createCard } from "metabase/lib/card";
diff --git a/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx b/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx
index 8e4254e413147e92b50c2597b5dbddfd73997282..ae3853de049591928f3167fb07426b14fa57507e 100644
--- a/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
index d7099a9d5f57a4590873b245350c6cf43b5e5169..3c3c73f632db068d7dd0bdd69490ee3a1f325172 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import S from "./ExpressionEditorTextfield.css";
 
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
index 19ebf2437291eb2ad60e2d23939d8b3b0b788c7f..4f592ba64c357abdaa519082a16e44cae9033d3f 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import cx from "classnames";
 import _ from 'underscore';
 
diff --git a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
index f2a1360e225c7b68c4a95b8d613c12d3eb6135f7..a65797a093eb9964c20136ebb8d773b71fe3b586 100644
--- a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import _ from "underscore";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx
index 66163c0abab7079247c339def3aced5bc98549d7..6d56e2b263b0e8d80d4a0d3be703303085023a95 100644
--- a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import "./TokenizedExpression.css";
 
diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx
index d02dfdcc2bb84b18cafe7d176a3a52b2a3ac7371..f431a6c542f00b146a34e34a33deb270b5ef7584 100644
--- a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import TokenizedExpression from "./TokenizedExpression.jsx";
diff --git a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
index 16eb3999fac7140f2e4c81a173ca548a267082a9..5aca73f9400d560f68e4dd0b1dddf7b17cf4fbfe 100644
--- a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import cx from "classnames";
 import { titleCase } from "humanize-plus";
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
index 08ebdef0e11d57cfc797da78d92f138f2451a677..b83df09ce9e0deab83d87be6c1c39caeb5a2413e 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { findDOMNode } from 'react-dom';
 
 import FilterWidget from './FilterWidget.jsx';
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
index 1f3460474b824cbce14b121188d7b1c930c2a0bd..8239053f68f3d5ed6f4e3ee143aedef348b746c6 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import FieldList from "../FieldList.jsx";
 import OperatorSelector from "./OperatorSelector.jsx";
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
index 495ea0ccc5d0c72f7f608dc6cc67e51f0b5cb296..274faf29b88153bb0edfc9f561974b1780d1c30c 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import FieldName from '../FieldName.jsx';
diff --git a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
index 4d5ce7a240fc237ba19ecda0b730b99a71392bbd..b6acdcbde667b1da3b90b8b1f7d05738d88f64cc 100644
--- a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
index bfb8e9cd2d56f47d3a05674c6a7435c0a4401392..c0152c54148cbdc7f2afc64b6e93fb346ef4cbce 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import SpecificDatePicker from "./SpecificDatePicker";
 import RelativeDatePicker, { DATE_PERIODS, UnitPicker } from "./RelativeDatePicker";
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
index 204594f76215c21ba2319f6c856d8f58fbd6a7a6..8fa6e7098ee5d13911898bdcb72c77c92139c8c0 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import TextPicker from "./TextPicker.jsx";
 
@@ -23,7 +24,13 @@ export default class NumberPicker extends Component<*, Props, State> {
     constructor(props: Props) {
         super(props);
         this.state = {
-            stringValues: props.values.map(v => String(v || "")),
+            stringValues: props.values.map(v => {
+                if(typeof v === 'number') {
+                    return String(v)
+                } else {
+                    return String(v || "")
+                }
+            }),
             validations: this._validate(props.values)
         }
     }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
index 589a529dc9c10ad0a879c10f09ca1b6ac84d7399..50be208d1497dc3402dfa607edd617d965950537 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { pluralize, titleCase, capitalize } from "humanize-plus";
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
index a91fdd0633f186000e974962a043fdc110c55e92..040d960c5489e5592ed54c597d01b8bc44275693 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import CheckBox from 'metabase/components/CheckBox.jsx';
 import ListSearchField from "metabase/components/ListSearchField.jsx";
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
index 8b5096629906e92327da0907e198583a51227298..83d9ab5ae3a18deb2a86f772c2a31395a86a3b2a 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 
 import Calendar from "metabase/components/Calendar";
 import Input from "metabase/components/Input";
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
index 90755d02ea841a28eba067041e3d21da0be57ede..29e58cb2e139783c01d7327e1500acf8287d4ae8 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, {Component, PropTypes} from "react";
+import React, {Component} from "react";
+import PropTypes from "prop-types";
 import AutosizeTextarea from 'react-textarea-autosize';
 
 import cx from "classnames";
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
index 4a43f13bf97862f1d7cadfc605e03e010a09064e..a8ecf63af1d755f9970901c6a616347137ce0197 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Code from "metabase/components/Code.jsx";
 
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
index fc385c58253f903783ba7c8a12e63dfe919e7577..75cc35c13acf85a134fcbb12305779dc4782f7c4 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import Toggle from "metabase/components/Toggle.jsx";
 import Select, { Option } from "metabase/components/Select.jsx";
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
index b1dadd198f9b755d50034d3747934360d678a21f..b969fcbd9e985811a1c885a84915c00a0f5a6802 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon.jsx";
 import TagEditorParam from "./TagEditorParam.jsx";
 import TagEditorHelp from "./TagEditorHelp.jsx";
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 95f367df9fa450f25cb94ddc88ed670c35a0138b..d934891be8cb6a19b532b1d3611cda766ee24d69 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
 import cx from "classnames";
@@ -154,9 +154,9 @@ export default class QueryBuilder extends Component {
 
         if (nextProps.location.action === "POP" && getURL(nextProps.location) !== getURL(this.props.location)) {
             this.props.popState(nextProps.location);
-        } else if (this.props.location.query.tutorial === undefined && nextProps.location.query.tutorial !== undefined) {
+        } else if (this.props.location.hash !== "#?tutorial" && nextProps.location.hash === "#?tutorial") {
             this.props.initializeQB(nextProps.location, nextProps.params);
-        } else if (getURL(nextProps.location) === "/q" && getURL(this.props.location) !== "/q") {
+        } else if (getURL(nextProps.location) === "/question" && getURL(this.props.location) !== "/question") {
             this.props.initializeQB(nextProps.location, nextProps.params);
         }
     }
diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
index d285d4c94d8dc3c1d6b1402e76bbc91ca5a3f36c..5ad4562fd5611ea0d4e2b96a4bacbfec9eab98fe 100644
--- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
+++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
@@ -1,11 +1,11 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { getParameters } from "metabase/meta/Card";
 import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams, } from "../actions";
diff --git a/frontend/src/metabase/questions/collections.js b/frontend/src/metabase/questions/collections.js
index 98ffd30aa0967674657997ae18af6a66f19d9069..592417105a325b7ba5ad1890a68ff19bc568216d 100644
--- a/frontend/src/metabase/questions/collections.js
+++ b/frontend/src/metabase/questions/collections.js
@@ -2,7 +2,7 @@
 import { createAction, createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
 import { reset } from 'redux-form';
 import { replace } from "react-router-redux";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/questions/components/ActionHeader.jsx b/frontend/src/metabase/questions/components/ActionHeader.jsx
index 041e8cf186daea51db034576691c5504c0b40ad9..a0eb0cea3e13c5b3c2f265158ebbaab2374e0411 100644
--- a/frontend/src/metabase/questions/components/ActionHeader.jsx
+++ b/frontend/src/metabase/questions/components/ActionHeader.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import S from "./ActionHeader.css";
 
 import StackedCheckBox from "metabase/components/StackedCheckBox.jsx";
diff --git a/frontend/src/metabase/questions/components/ArchivedItem.jsx b/frontend/src/metabase/questions/components/ArchivedItem.jsx
index bcfe9200e77f7525f589ed616cafb90fa572aeda..babb93cfedfe2d20fc31bbee6677666baaca7bc4 100644
--- a/frontend/src/metabase/questions/components/ArchivedItem.jsx
+++ b/frontend/src/metabase/questions/components/ArchivedItem.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon";
 import Tooltip from "metabase/components/Tooltip";
diff --git a/frontend/src/metabase/questions/components/CollectionBadge.jsx b/frontend/src/metabase/questions/components/CollectionBadge.jsx
index 143520b64333f5aa63cb28db9bc50eef5d7dfa91..42711f9f18a02d4dde90ea27c39e2d8c22467fbe 100644
--- a/frontend/src/metabase/questions/components/CollectionBadge.jsx
+++ b/frontend/src/metabase/questions/components/CollectionBadge.jsx
@@ -1,7 +1,7 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 import { Link } from "react-router";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import Color from "color";
 import cx from "classnames";
diff --git a/frontend/src/metabase/questions/components/CollectionButtons.jsx b/frontend/src/metabase/questions/components/CollectionButtons.jsx
index a27c6f4a61b0b6af4d53f2ad3a2d6dabfaa58d6b..ce2b246ec29085b3738c6aaa597d7c423232a2a5 100644
--- a/frontend/src/metabase/questions/components/CollectionButtons.jsx
+++ b/frontend/src/metabase/questions/components/CollectionButtons.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
index 51ae5c303dad0d1ddefafb41b9fb0155addf4aac..b2583c2c48d7af90b15fbb8df24049270b9a3825 100644
--- a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
+++ b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import cx from "classnames";
diff --git a/frontend/src/metabase/questions/components/Item.jsx b/frontend/src/metabase/questions/components/Item.jsx
index b8f5003d4fd7c20809de6f5504c96cc032442fd3..533016e71cbe52afa983ed79b5bdc7734c6bea67 100644
--- a/frontend/src/metabase/questions/components/Item.jsx
+++ b/frontend/src/metabase/questions/components/Item.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import cx from "classnames";
 import pure from "recompose/pure";
@@ -14,7 +15,7 @@ import MoveToCollection from "../containers/MoveToCollection.jsx";
 import Labels from "./Labels.jsx";
 import CollectionBadge from "./CollectionBadge.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 const ITEM_ICON_SIZE = 20;
 
@@ -122,7 +123,7 @@ Item.propTypes = {
 const ItemBody = pure(({ entity, id, name, description, labels, favorite, collection, setFavorited, onEntityClick }) =>
     <div className={S.itemBody}>
         <div className={cx('flex', S.itemTitle)}>
-            <Link to={Urls.card(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}>
+            <Link to={Urls.question(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}>
                 {name}
             </Link>
             { collection &&
diff --git a/frontend/src/metabase/questions/components/LabelIconPicker.jsx b/frontend/src/metabase/questions/components/LabelIconPicker.jsx
index c78d4898592af89e481c592eecbfc266d96e32d8..fe58e7058ac4253ac63d81465fc181aa7cecd813 100644
--- a/frontend/src/metabase/questions/components/LabelIconPicker.jsx
+++ b/frontend/src/metabase/questions/components/LabelIconPicker.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import S from "./LabelIconPicker.css";
 
diff --git a/frontend/src/metabase/questions/components/LabelPicker.jsx b/frontend/src/metabase/questions/components/LabelPicker.jsx
index 36d6645ea2c20a57a53ff8e9ae1113deb4d6c671..f835089581eaac5ac9a132f28971fca534f79f68 100644
--- a/frontend/src/metabase/questions/components/LabelPicker.jsx
+++ b/frontend/src/metabase/questions/components/LabelPicker.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import S from "./LabelPicker.css";
diff --git a/frontend/src/metabase/questions/components/Labels.jsx b/frontend/src/metabase/questions/components/Labels.jsx
index cadf7680243fc2acd3dc436d3c45eac04b524f1f..87f822aa96cbe4833aa66ce63519a43dd93568df 100644
--- a/frontend/src/metabase/questions/components/Labels.jsx
+++ b/frontend/src/metabase/questions/components/Labels.jsx
@@ -1,9 +1,10 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./Labels.css";
 import color from 'color'
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import EmojiIcon from "metabase/components/EmojiIcon.jsx"
 
diff --git a/frontend/src/metabase/questions/components/List.jsx b/frontend/src/metabase/questions/components/List.jsx
index a02ba7d094451095e2a219924479c5f576d1c3e6..cc49ac1cf0ab94d1ff5773371f55b8ae442c9533 100644
--- a/frontend/src/metabase/questions/components/List.jsx
+++ b/frontend/src/metabase/questions/components/List.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 
 import S from "./List.css";
 import pure from "recompose/pure";
diff --git a/frontend/src/metabase/questions/components/SearchHeader.jsx b/frontend/src/metabase/questions/components/SearchHeader.jsx
index abf67231b4c3e7d5af25570b0c216f2146a5172d..2014b6603b9dfb2ade2691eb12ff324370280245 100644
--- a/frontend/src/metabase/questions/components/SearchHeader.jsx
+++ b/frontend/src/metabase/questions/components/SearchHeader.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import S from "./SearchHeader.css";
 
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
index 63e94607239152cadd83a7460f73312604814767..3fc8b913ec6a795af113601f8e55110d7fb76786 100644
--- a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
+++ b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
diff --git a/frontend/src/metabase/questions/containers/CollectionCreate.jsx b/frontend/src/metabase/questions/containers/CollectionCreate.jsx
index d9da2e02dd9fc1cf2e17af946b2677a1e71a36a5..edb1e6a93591ac30b71f849e12987d92034fa286 100644
--- a/frontend/src/metabase/questions/containers/CollectionCreate.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionCreate.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
diff --git a/frontend/src/metabase/questions/containers/CollectionEdit.jsx b/frontend/src/metabase/questions/containers/CollectionEdit.jsx
index bf87420bc608b58715e444bf74e6c55ba9559260..b297607024bd6d59d9fe4f7aaa00af56314bc42d 100644
--- a/frontend/src/metabase/questions/containers/CollectionEdit.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionEdit.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { connect } from "react-redux";
 import { goBack } from "react-router-redux";
diff --git a/frontend/src/metabase/questions/containers/CollectionPage.jsx b/frontend/src/metabase/questions/containers/CollectionPage.jsx
index 45f73d2de67caf8169bab7415611dd44dd58f398..c69b15be2244a3fd6a3394eeaf68a405e4f03789 100644
--- a/frontend/src/metabase/questions/containers/CollectionPage.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionPage.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push, replace, goBack } from "react-router-redux";
 
diff --git a/frontend/src/metabase/questions/containers/EditLabels.jsx b/frontend/src/metabase/questions/containers/EditLabels.jsx
index 45b3aaf0bce084634ae167659f656d9890b068a3..065e38a06d2a68848f67b4a4c361d48faf58d395 100644
--- a/frontend/src/metabase/questions/containers/EditLabels.jsx
+++ b/frontend/src/metabase/questions/containers/EditLabels.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import S from "./EditLabels.css";
diff --git a/frontend/src/metabase/questions/containers/EntityItem.jsx b/frontend/src/metabase/questions/containers/EntityItem.jsx
index 42882525c8b39a4ed1e948b8e3ef6e9e22cf5c07..12b839966c9e02d383383c0620486ac611535880 100644
--- a/frontend/src/metabase/questions/containers/EntityItem.jsx
+++ b/frontend/src/metabase/questions/containers/EntityItem.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import Item from "../components/Item.jsx";
diff --git a/frontend/src/metabase/questions/containers/EntityList.jsx b/frontend/src/metabase/questions/containers/EntityList.jsx
index 67cc28dfb991d4f02eb83880a22a790ce9453edd..b06cc645fec8a4b0926a0e50f3543ea828433655 100644
--- a/frontend/src/metabase/questions/containers/EntityList.jsx
+++ b/frontend/src/metabase/questions/containers/EntityList.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
 
diff --git a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
index 82f34a8191c59471f96615c8cc553765e80ce992..b1954d1ab91a3ab528fc895323fa28aeaf398f58 100644
--- a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
+++ b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import S from "./LabelEditorForm.css";
 
 import LabelIconPicker from "../components/LabelIconPicker.jsx";
diff --git a/frontend/src/metabase/questions/containers/LabelPopover.jsx b/frontend/src/metabase/questions/containers/LabelPopover.jsx
index b1c67461ba9e8c6c31141daab2f15298ffd3b019..7530929de177fda583887c2bbbf593e44e74c364 100644
--- a/frontend/src/metabase/questions/containers/LabelPopover.jsx
+++ b/frontend/src/metabase/questions/containers/LabelPopover.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
diff --git a/frontend/src/metabase/questions/containers/QuestionIndex.jsx b/frontend/src/metabase/questions/containers/QuestionIndex.jsx
index e65f45470f0db9abf3497fc9ed8f43afe4161d92..e0b95acbc50cd9ce174d5086c5c9b789af5a5d2f 100644
--- a/frontend/src/metabase/questions/containers/QuestionIndex.jsx
+++ b/frontend/src/metabase/questions/containers/QuestionIndex.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 import { Link } from "react-router";
 import Collapse from "react-collapse";
diff --git a/frontend/src/metabase/questions/questions.js b/frontend/src/metabase/questions/questions.js
index 9d1970c246abe09a32a35cda55ae91100af7ecfe..056815d94ee2c1828401aafc862f112d7af16502 100644
--- a/frontend/src/metabase/questions/questions.js
+++ b/frontend/src/metabase/questions/questions.js
@@ -7,7 +7,7 @@ import _ from "underscore";
 
 import { inflect } from "metabase/lib/formatting";
 import MetabaseAnalytics from "metabase/lib/analytics";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { push, replace } from "react-router-redux";
 import { setRequestState } from "metabase/redux/requests";
diff --git a/frontend/src/metabase/reference/components/Detail.jsx b/frontend/src/metabase/reference/components/Detail.jsx
index c6d76d55c70bd1ab5071e1f9cf371e00e069eca0..210d4f11167d328deb124402eb486cc10a46dff5 100644
--- a/frontend/src/metabase/reference/components/Detail.jsx
+++ b/frontend/src/metabase/reference/components/Detail.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./Detail.css";
 
diff --git a/frontend/src/metabase/reference/components/EditButton.jsx b/frontend/src/metabase/reference/components/EditButton.jsx
index fe3a34779fcd078bd9b067f3beb973177fcdca79..81d2513fef3b9ce4addac03620a50da0c6924aba 100644
--- a/frontend/src/metabase/reference/components/EditButton.jsx
+++ b/frontend/src/metabase/reference/components/EditButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
 
diff --git a/frontend/src/metabase/reference/components/EditHeader.jsx b/frontend/src/metabase/reference/components/EditHeader.jsx
index 116e82143a8300db3619a6d93b04d01bfbe5b422..5434e4844195886fc8cf6c158f24515f9ccc0e21 100644
--- a/frontend/src/metabase/reference/components/EditHeader.jsx
+++ b/frontend/src/metabase/reference/components/EditHeader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
 
diff --git a/frontend/src/metabase/reference/components/Field.jsx b/frontend/src/metabase/reference/components/Field.jsx
index 7c2770a99ba320b87db62876f550c63594098fb5..c5c020605f3b18cd830e4b1bcb0fc0377da4e6d0 100644
--- a/frontend/src/metabase/reference/components/Field.jsx
+++ b/frontend/src/metabase/reference/components/Field.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import * as MetabaseCore from "metabase/lib/core";
diff --git a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
index 33fb42cbb1bc9a86eedc6d03cdcd435d8a58f3df..87852ec59defb46f0a006b2f2f0d58ef2af136c4 100644
--- a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
+++ b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import pure from "recompose/pure";
 
 import S from "./FieldToGroupBy.css";
@@ -42,4 +43,4 @@ FieldToGroupBy.propTypes = {
     secondaryOnClick: PropTypes.func
 };
 
-export default pure(FieldToGroupBy);
\ No newline at end of file
+export default pure(FieldToGroupBy);
diff --git a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
index ce5cda7e62e02966928188a31b4f8c6e1c722082..c42ab2572e4fcd38901e1f44b86a80d73b4be7bd 100644
--- a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
+++ b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import { getIn } from "icepick";
 import pure from "recompose/pure";
diff --git a/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx
index e5b77572f10ff70486b11e751e53d45baf64bd48..48cd1535bfbd2e533b04ade920f101820355a8f3 100644
--- a/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx
+++ b/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
 
diff --git a/frontend/src/metabase/reference/components/Formula.jsx b/frontend/src/metabase/reference/components/Formula.jsx
index 04819cf39f8425f0150268be20c896268802c961..4882a4d1b741312af2c100be4e5c21803db372f2 100644
--- a/frontend/src/metabase/reference/components/Formula.jsx
+++ b/frontend/src/metabase/reference/components/Formula.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import pure from "recompose/pure";
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/reference/components/GuideDetail.jsx b/frontend/src/metabase/reference/components/GuideDetail.jsx
index bbb27cc09b74ceff2c85ff22c45cd4925aa6add1..45b547e95adb026958df62b0c2283c582c1375d2 100644
--- a/frontend/src/metabase/reference/components/GuideDetail.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetail.jsx
@@ -1,8 +1,11 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import pure from "recompose/pure";
 import cx from "classnames";
+
 import Icon from "metabase/components/Icon"
+import * as Urls from "metabase/lib/urls";
 
 import {
     getQuestionUrl,
@@ -21,7 +24,7 @@ const GuideDetail = ({
     const title = entity.display_name || entity.name;
     const { caveats, points_of_interest } = entity;
     const typeToLink = {
-        dashboard: `/dash/${entity.id}`,
+        dashboard: Urls.dashboard(entity.id),
         metric: getQuestionUrl({
             dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
             tableId: entity.table_id,
diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
index cc3eb93c86938cd50f407ac933ca224d2971f84e..cd82f2e410b31dd39156855fb6714f02a20b4b88 100644
--- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 // FIXME: using pure seems to mess with redux form updates
 // import pure from "recompose/pure";
 import cx from "classnames";
diff --git a/frontend/src/metabase/reference/components/GuideEditSection.jsx b/frontend/src/metabase/reference/components/GuideEditSection.jsx
index f8e416a928dd48be5191f6fa7068da770931c65e..f3b30093b2448bb13c0d610ae384f176fe22eee5 100644
--- a/frontend/src/metabase/reference/components/GuideEditSection.jsx
+++ b/frontend/src/metabase/reference/components/GuideEditSection.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import pure from "recompose/pure";
 import cx from "classnames";
diff --git a/frontend/src/metabase/reference/components/GuideHeader.jsx b/frontend/src/metabase/reference/components/GuideHeader.jsx
index 28a37f070506960c1adeef68496bc3646248381f..863e7d4f08305f80434755a1079647d5037327a1 100644
--- a/frontend/src/metabase/reference/components/GuideHeader.jsx
+++ b/frontend/src/metabase/reference/components/GuideHeader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import pure from "recompose/pure";
 
 import EditButton from "metabase/reference/components/EditButton.jsx";
diff --git a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
index 1fbf5afb469053cacad2a40fade6fabefaa5e3cd..f21e13d63269a23865f5366b2976204a8079c85a 100644
--- a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
+++ b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
 
diff --git a/frontend/src/metabase/reference/components/ReferenceHeader.jsx b/frontend/src/metabase/reference/components/ReferenceHeader.jsx
index 21a718a30e020e3748fb145455e567b326cf6a4e..9a5fcfedde558e2b70d27cc1fa772aa510297e15 100644
--- a/frontend/src/metabase/reference/components/ReferenceHeader.jsx
+++ b/frontend/src/metabase/reference/components/ReferenceHeader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import cx from "classnames";
 import pure from "recompose/pure";
diff --git a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
index 5e052780d7d29b164ac7bb1f3815bb4e44e6d770..282e4f54b0a8ddde81a6a8409f9831d71b9caf64 100644
--- a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
+++ b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
diff --git a/frontend/src/metabase/reference/components/UsefulQuestions.jsx b/frontend/src/metabase/reference/components/UsefulQuestions.jsx
index 0c7e49a463a77860fca9fb1122de35f00f27b1b2..bac4d8e2bcbd5ed42f67312ba03cd5ba7160784d 100644
--- a/frontend/src/metabase/reference/components/UsefulQuestions.jsx
+++ b/frontend/src/metabase/reference/components/UsefulQuestions.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
 
diff --git a/frontend/src/metabase/reference/containers/ReferenceApp.jsx b/frontend/src/metabase/reference/containers/ReferenceApp.jsx
index f89d6b700e4d931c4d466fabb74049a8a085986e..dbb904d0a65fc2e7d2e8b5a85799b7aef51a349a 100644
--- a/frontend/src/metabase/reference/containers/ReferenceApp.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceApp.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from 'react';
+import React, { Component } from 'react';
+import PropTypes from "prop-types";
 import { connect } from 'react-redux';
 
 import Sidebar from 'metabase/components/Sidebar.jsx';
diff --git a/frontend/src/metabase/reference/containers/ReferenceEntity.jsx b/frontend/src/metabase/reference/containers/ReferenceEntity.jsx
index b5d43d664d6c7a0c585bf59b9e4fe373ba92883b..45a0b6ec4ac7f76d968d2fa79cae07b745639607 100644
--- a/frontend/src/metabase/reference/containers/ReferenceEntity.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceEntity.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
diff --git a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
index daf3818bd173b97955d390ef587e863b4299d9a7..3773c59cf77c34e39bcdaf1bda60ddd36d2ec68e 100644
--- a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
@@ -1,10 +1,12 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
 
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
+import * as Urls from "metabase/lib/urls";
 
 import S from "metabase/components/List.css";
 import R from "metabase/reference/Reference.css";
@@ -57,7 +59,7 @@ const createListItem = (entity, index, section) =>
             }
             url={section.type !== 'questions' ?
                 `${section.id}/${entity.id}` :
-                `/card/${entity.id}`
+                Urls.question(entity.id)
             }
             icon={section.type === 'questions' ?
                 visualizations.get(entity.display).iconName :
diff --git a/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx b/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx
index b680dba9f4a54aa3a360db4bd9ad12c881bd8e75..a5aebda8df7c32abe0e925d300af48dd7f7493f4 100644
--- a/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 
diff --git a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
index a5640fb8d7295fb1b3d616207ce54a3a38edbcef..9cdedc4cc97b2a64fb9b4029c58181ca174149c2 100644
--- a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
@@ -1,13 +1,16 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 import { connect } from 'react-redux';
 import { push } from 'react-router-redux';
 import { reduxForm } from "redux-form";
+
 import { assoc } from "icepick";
 import cx from "classnames";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
+import * as Urls from "metabase/lib/urls";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx';
@@ -225,7 +228,7 @@ export default class ReferenceGettingStartedGuide extends Component {
                             createDashboardFn={async (newDashboard) => {
                                 try {
                                     const action = await createDashboard(newDashboard, true);
-                                    push(`/dash/${action.payload.id}`);
+                                    push(Urls.dashboard(action.payload.id));
                                 }
                                 catch(error) {
                                     console.error(error);
@@ -632,4 +635,4 @@ const AdminInstructions = ({ children }) => // eslint-disable-line react/prop-ty
     </div>
 
 const SectionHeader = ({ trim, children }) => // eslint-disable-line react/prop-types
-    <h2 className={cx('text-dark text-measure', {  "mb0" : trim }, { "mb4" : !trim })}>{children}</h2> 
+    <h2 className={cx('text-dark text-measure', {  "mb0" : trim }, { "mb4" : !trim })}>{children}</h2>
diff --git a/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx b/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx
index 192b6b45e583467d2866db37a41a1228a9e17642..05465289ba7289c2adf13f311541b27a6c790217 100644
--- a/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import { getIn } from "icepick";
diff --git a/frontend/src/metabase/reference/utils.js b/frontend/src/metabase/reference/utils.js
index e22840da4f48bf99e05eb563c75c6951927bee43..0a85e12cc89dcc5ec8ef40b417179cc031cae011 100644
--- a/frontend/src/metabase/reference/utils.js
+++ b/frontend/src/metabase/reference/utils.js
@@ -2,8 +2,9 @@ import { assoc, assocIn, chain } from "icepick";
 import _ from "underscore";
 
 import { titleize, humanize } from "metabase/lib/formatting";
-import { startNewCard, serializeCardForUrl } from "metabase/lib/card";
+import { startNewCard } from "metabase/lib/card";
 import { isPK } from "metabase/lib/types";
+import * as Urls from "metabase/lib/urls";
 
 export const idsToObjectMap = (ids, objects) => ids
     .map(id => objects[id])
@@ -76,11 +77,10 @@ export const tryUpdateData = async (fields, props) => {
                 const importantFieldIds = fields.important_fields.map(field => field.id);
                 const existingImportantFieldIds = guide.metric_important_fields && guide.metric_important_fields[entity.id];
 
-                const areFieldIdsIdentitical = existingImportantFieldIds && 
+                const areFieldIdsIdentitical = existingImportantFieldIds &&
                     existingImportantFieldIds.length === importantFieldIds.length &&
                     existingImportantFieldIds.every(id => importantFieldIds.includes(id));
-                
-                console.log(areFieldIdsIdentitical);
+
                 if (!areFieldIdsIdentitical) {
                     await updateMetricImportantFields(entity.id, importantFieldIds);
                     tryFetchData(props);
@@ -156,8 +156,8 @@ export const tryUpdateGuide = async (formFields, props) => {
     startLoading();
     try {
         const updateNewEntities = ({
-            entities, 
-            formFields, 
+            entities,
+            formFields,
             updateEntity
         }) => formFields.map(formField => {
             if (!formField.id) {
@@ -175,7 +175,7 @@ export const tryUpdateGuide = async (formFields, props) => {
 
             const newEntity = entities[formField.id];
             const updatedNewEntity = {
-                ...newEntity, 
+                ...newEntity,
                 ...editedEntity
             };
 
@@ -185,9 +185,9 @@ export const tryUpdateGuide = async (formFields, props) => {
         });
 
         const updateOldEntities = ({
-            newEntityIds, 
-            oldEntityIds, 
-            entities, 
+            newEntityIds,
+            oldEntityIds,
+            entities,
             updateEntity
         }) => oldEntityIds
             .filter(oldEntityId => !newEntityIds.includes(oldEntityId))
@@ -201,14 +201,14 @@ export const tryUpdateGuide = async (formFields, props) => {
                 );
 
                 const updatingOldEntity = updateEntity(updatedOldEntity);
-                
+
                 return [updatingOldEntity];
             });
         //FIXME: necessary because revision_message is a mandatory field
         // even though we don't actually keep track of changes to caveats/points_of_interest yet
         const updateWithRevisionMessage = updateEntity => entity => updateEntity(assoc(
             entity,
-            'revision_message', 
+            'revision_message',
             'Updated in Getting Started guide.'
         ));
 
@@ -218,9 +218,9 @@ export const tryUpdateGuide = async (formFields, props) => {
                 updateEntity: updateDashboard
             })
             .concat(updateOldEntities({
-                newEntityIds: formFields.most_important_dashboard ? 
+                newEntityIds: formFields.most_important_dashboard ?
                     [formFields.most_important_dashboard.id] : [],
-                oldEntityIds: guide.most_important_dashboard ? 
+                oldEntityIds: guide.most_important_dashboard ?
                     [guide.most_important_dashboard] :
                     [],
                 entities: dashboards,
@@ -239,7 +239,7 @@ export const tryUpdateGuide = async (formFields, props) => {
                 entities: metrics,
                 updateEntity: updateWithRevisionMessage(updateMetric)
             }));
-        
+
         const updatingMetricImportantFields = formFields.important_metrics
             .map(metricFormField => {
                 if (!metricFormField.id || !metricFormField.important_fields) {
@@ -248,17 +248,17 @@ export const tryUpdateGuide = async (formFields, props) => {
                 const importantFieldIds = metricFormField.important_fields
                     .map(field => field.id);
                 const existingImportantFieldIds = guide.metric_important_fields[metricFormField.id];
-                
-                const areFieldIdsIdentitical = existingImportantFieldIds && 
+
+                const areFieldIdsIdentitical = existingImportantFieldIds &&
                     existingImportantFieldIds.length === importantFieldIds.length &&
                     existingImportantFieldIds.every(id => importantFieldIds.includes(id));
                 if (areFieldIdsIdentitical) {
                     return [];
                 }
-                
+
                 return [updateMetricImportantFields(metricFormField.id, importantFieldIds)];
             });
-        
+
         const segmentFields = formFields.important_segments_and_tables
             .filter(field => field.type === 'segment');
 
@@ -299,7 +299,7 @@ export const tryUpdateGuide = async (formFields, props) => {
             guide.contact.name !== formFields.contact.name ?
                 [updateSetting({key: 'getting-started-contact-name', value: formFields.contact.name })] :
                 [];
-        
+
         const updatingContactEmail = guide.contact && formFields.contact &&
             guide.contact.email !== formFields.contact.email ?
                 [updateSetting({key: 'getting-started-contact-email', value: formFields.contact.email })] :
@@ -318,7 +318,7 @@ export const tryUpdateGuide = async (formFields, props) => {
 
         if (updatingData.length > 0) {
             await Promise.all(updatingData);
-            
+
             clearRequestState({statePath: ['reference', 'guide']});
 
             await fetchGuide();
@@ -431,7 +431,7 @@ export const getQuestion = ({dbId, tableId, fieldId, metricId, segmentId, getCou
     return question;
 };
 
-export const getQuestionUrl = getQuestionArgs => `/q#${serializeCardForUrl(getQuestion(getQuestionArgs))}`;
+export const getQuestionUrl = getQuestionArgs => Urls.question(null, getQuestion(getQuestionArgs));
 
 export const isGuideEmpty = ({
     things_to_know,
@@ -446,7 +446,7 @@ export const isGuideEmpty = ({
     most_important_dashboard ? false :
     important_metrics && important_metrics.length !== 0 ? false :
     important_segments && important_segments.length !== 0 ? false :
-    important_tables && important_tables.length !== 0 ? false : 
+    important_tables && important_tables.length !== 0 ? false :
     true;
 
 export const typeToLinkClass = {
diff --git a/frontend/src/metabase/routes-embed.jsx b/frontend/src/metabase/routes-embed.jsx
index 96e5de25976a2d133116f2ebab42766940c6d3e0..6697785f74855e4906bdd4925979c69bc472c387 100644
--- a/frontend/src/metabase/routes-embed.jsx
+++ b/frontend/src/metabase/routes-embed.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { Route } from 'react-router';
 
diff --git a/frontend/src/metabase/routes-public.jsx b/frontend/src/metabase/routes-public.jsx
index 0ed8e87dd228c8d5350d3f55f7e50527e9b56c68..f40e59cec01b8a3ca84cb667d5d49f9526a57be4 100644
--- a/frontend/src/metabase/routes-public.jsx
+++ b/frontend/src/metabase/routes-public.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { Route } from 'react-router';
 
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index 26285e188981bda6c8cadddadaa8053905382c08..f52854442201ee62eae1d9693c4d623e074c5099 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { Route, Redirect, IndexRedirect, IndexRoute } from 'react-router';
 import { routerActions } from 'react-router-redux';
@@ -151,11 +151,11 @@ export const getRoutes = (store) =>
                 <Route path="/" component={HomepageApp} />
 
                 {/* DASHBOARD */}
-                <Route path="/dash/:dashboardId" component={DashboardApp} />
+                <Route path="/dashboard/:dashboardId" component={DashboardApp} />
 
                 {/* QUERY BUILDER */}
-                <Route path="/card/:cardId" component={QueryBuilder} />
-                <Route path="/q" component={QueryBuilder} />
+                <Route path="/question" component={QueryBuilder} />
+                <Route path="/question/:cardId" component={QueryBuilder} />
 
                 {/* QUESTIONS */}
                 <Route path="/questions">
@@ -262,13 +262,13 @@ export const getRoutes = (store) =>
                 <IndexRedirect to="/_internal/list" />
             </Route>
 
+            {/* DEPRECATED */}
+            <Redirect from="/q" to="/question" />
+            <Redirect from="/card/:cardId" to="/question/:cardId" />
+            <Redirect from="/dash/:dashboardId" to="/dashboard/:dashboardId" />
+
             {/* MISC */}
             <Route path="/unauthorized" component={Unauthorized} />
             <Route path="/*" component={NotFound} />
-
-            {/* LEGACY */}
-            <Redirect from="/card" to="/questions" />
-            <Redirect from="/card/:cardId/:serializedCard" to="/questions/:cardId#:serializedCard" />
-            <Redirect from="/q/:serializedCard" to="/q#:serializedCard" />
         </Route>
     </Route>
diff --git a/frontend/src/metabase/setup/components/CollapsedStep.jsx b/frontend/src/metabase/setup/components/CollapsedStep.jsx
index 90ecad4374d914cc10adc81ceab900dec7a0a9e3..34501e614341736a446b9199f99fcd0550d8196f 100644
--- a/frontend/src/metabase/setup/components/CollapsedStep.jsx
+++ b/frontend/src/metabase/setup/components/CollapsedStep.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/setup/components/DatabaseStep.jsx b/frontend/src/metabase/setup/components/DatabaseStep.jsx
index 5bf60b9c2340522889f31d8ed4dcae340403884b..9ec39c01afd0de087400196c1d138cb70e9317f8 100644
--- a/frontend/src/metabase/setup/components/DatabaseStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseStep.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import StepTitle from './StepTitle.jsx'
diff --git a/frontend/src/metabase/setup/components/PreferencesStep.jsx b/frontend/src/metabase/setup/components/PreferencesStep.jsx
index 70a21fe00bca237c6ff46e3d2c2abdae6a931c00..bac1d80bcdcceb24ac04b1402bbdb4d6f42bfe0a 100644
--- a/frontend/src/metabase/setup/components/PreferencesStep.jsx
+++ b/frontend/src/metabase/setup/components/PreferencesStep.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
diff --git a/frontend/src/metabase/setup/components/Setup.jsx b/frontend/src/metabase/setup/components/Setup.jsx
index 0437ae223d8f347d212824f1b20ecdf9441c0c09..4c99d49bd281a92bb099203516d1052c5354c5b4 100644
--- a/frontend/src/metabase/setup/components/Setup.jsx
+++ b/frontend/src/metabase/setup/components/Setup.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 import LogoIcon from 'metabase/components/LogoIcon.jsx';
diff --git a/frontend/src/metabase/setup/components/StepTitle.jsx b/frontend/src/metabase/setup/components/StepTitle.jsx
index 3f1cf52ba8cc5521987b58e5bbc109ab0ace4e53..48531185c97e616b2f6f8e9d1d36a805c4e18eea 100644
--- a/frontend/src/metabase/setup/components/StepTitle.jsx
+++ b/frontend/src/metabase/setup/components/StepTitle.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon.jsx";
 
 export default class StepTitle extends Component {
diff --git a/frontend/src/metabase/setup/components/UserStep.jsx b/frontend/src/metabase/setup/components/UserStep.jsx
index 43d5f2258d2d480a6fce0cf3b0aa86c557f244b3..24c31440ee1b0042abe5af605efca2e3c5a0c7f0 100644
--- a/frontend/src/metabase/setup/components/UserStep.jsx
+++ b/frontend/src/metabase/setup/components/UserStep.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import FormField from "metabase/components/form/FormField.jsx";
diff --git a/frontend/src/metabase/setup/containers/SetupApp.jsx b/frontend/src/metabase/setup/containers/SetupApp.jsx
index bd9a7dcba0e481452e87fb0e1977b1af71340859..aa2f5617c9af0dc43021b64b5621a47d1d5b2968 100644
--- a/frontend/src/metabase/setup/containers/SetupApp.jsx
+++ b/frontend/src/metabase/setup/containers/SetupApp.jsx
@@ -1,5 +1,5 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import Setup from "../components/Setup.jsx";
diff --git a/frontend/src/metabase/tutorial/PageFlag.jsx b/frontend/src/metabase/tutorial/PageFlag.jsx
index 2f9e7e328dfaac869f67053de5e65d61fbfe85e9..56c1ca977cf9a24f90a1cc2926f01393a6df9cca 100644
--- a/frontend/src/metabase/tutorial/PageFlag.jsx
+++ b/frontend/src/metabase/tutorial/PageFlag.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import "./PageFlag.css";
 
diff --git a/frontend/src/metabase/tutorial/Portal.jsx b/frontend/src/metabase/tutorial/Portal.jsx
index 052d4d7e092e1adf7f2b4c896600c35de0d38859..150e19b27f15a8ab6e8bffa342f710c5b551636d 100644
--- a/frontend/src/metabase/tutorial/Portal.jsx
+++ b/frontend/src/metabase/tutorial/Portal.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import BodyComponent from "metabase/components/BodyComponent.jsx";
 
diff --git a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
index ad9d0c1817b5c136d80f3e8c094ea6b849622aaf..24638a3850d7ec602f9cc23d1af9a0047c4473cc 100644
--- a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
+++ b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
@@ -1,6 +1,6 @@
 /* eslint-disable react/display-name */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Tutorial, { qs, qsWithContent } from "./Tutorial.jsx";
 
diff --git a/frontend/src/metabase/tutorial/Tutorial.jsx b/frontend/src/metabase/tutorial/Tutorial.jsx
index 059b17e514f9c22eaf683a44b10d997d433ae2c8..81066e500a9f72c96f350a131ba899d48c484177 100644
--- a/frontend/src/metabase/tutorial/Tutorial.jsx
+++ b/frontend/src/metabase/tutorial/Tutorial.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Modal from "metabase/components/Modal.jsx";
 import Popover from "metabase/components/Popover.jsx";
diff --git a/frontend/src/metabase/tutorial/TutorialModal.jsx b/frontend/src/metabase/tutorial/TutorialModal.jsx
index 46037b83d126c47be500a9826d64531c896fac50..b1c2c73c5db9ebd689034bc2530fe5f450df5e5a 100644
--- a/frontend/src/metabase/tutorial/TutorialModal.jsx
+++ b/frontend/src/metabase/tutorial/TutorialModal.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
diff --git a/frontend/src/metabase/user/components/SetUserPassword.jsx b/frontend/src/metabase/user/components/SetUserPassword.jsx
index bcb2c03c3771116bd84d3edf69d467412398288b..85af677fc4ede5f7dbd02e211fb6086c2aa76831 100644
--- a/frontend/src/metabase/user/components/SetUserPassword.jsx
+++ b/frontend/src/metabase/user/components/SetUserPassword.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import FormField from "metabase/components/form/FormField.jsx";
diff --git a/frontend/src/metabase/user/components/UpdateUserDetails.jsx b/frontend/src/metabase/user/components/UpdateUserDetails.jsx
index c5950c24fb0bb5608d98946f78b3bdeb4eb9d01c..acff1daecbc5e3fabb3eb72368626272fd42ce13 100644
--- a/frontend/src/metabase/user/components/UpdateUserDetails.jsx
+++ b/frontend/src/metabase/user/components/UpdateUserDetails.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import FormField from "metabase/components/form/FormField.jsx";
diff --git a/frontend/src/metabase/user/components/UserSettings.jsx b/frontend/src/metabase/user/components/UserSettings.jsx
index a06bc0807cf2e1d45ca535b1f27c8aa9ae6323da..036ff8898b190217e7a2ae22610ef2bd5533cada 100644
--- a/frontend/src/metabase/user/components/UserSettings.jsx
+++ b/frontend/src/metabase/user/components/UserSettings.jsx
@@ -1,5 +1,6 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import cx from "classnames";
 
 import SetUserPassword from "./SetUserPassword.jsx";
diff --git a/frontend/src/metabase/user/containers/UserSettingsApp.jsx b/frontend/src/metabase/user/containers/UserSettingsApp.jsx
index 971d547953f06267eb1f79694deb960dda97b736..839412015a88510ecb1a1e01e21df1eadea18d58 100644
--- a/frontend/src/metabase/user/containers/UserSettingsApp.jsx
+++ b/frontend/src/metabase/user/containers/UserSettingsApp.jsx
@@ -1,5 +1,5 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import UserSettings from "../components/UserSettings.jsx";
diff --git a/frontend/src/metabase/visualizations/components/CardRenderer.jsx b/frontend/src/metabase/visualizations/components/CardRenderer.jsx
index 223184ce7592bed7cae9c4abcdd023dd6c35d80f..545881c62ee9fd4451bd97e1d2533339b0e1b11e 100644
--- a/frontend/src/metabase/visualizations/components/CardRenderer.jsx
+++ b/frontend/src/metabase/visualizations/components/CardRenderer.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import ExplicitSize from "metabase/components/ExplicitSize.jsx";
diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
index 0e3563eb51714831c71312168f6b487cb245f326..87c0c1d05c003ab1f4ba629c261c6df0d34e7ddf 100644
--- a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Button from "metabase/components/Button";
 import Popover from "metabase/components/Popover";
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index a7dce968035f0f5dedfa84e521392746a52d53b3..15e4f01eb7756e53560dce726c4a7eccbee05e5d 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import cx from "classnames";
 import { assocIn } from "icepick";
 import _ from "underscore";
diff --git a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
index 501b403030d05f48af9fe7fc1b59bb937f742384..71d2383a483ad50f68bd92d8f7fa1c2d9975f003 100644
--- a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import TooltipPopover from "metabase/components/TooltipPopover.jsx"
 import Value from "metabase/components/Value.jsx";
diff --git a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
index 9218edfcb3009c2e5de766cbaa5f26a76b66ecfb..4cb2efaf16edf22f8c1dfce215957276335f149d 100644
--- a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import styles from "./ChartWithLegend.css";
 
 import LegendVertical from "./LegendVertical.jsx";
diff --git a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
index d390fa5d61203b5a6877e1c46ec9e2596bac3563..d576bd738b37e7a266a64dfc710bb79bca200037 100644
--- a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
+++ b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 
diff --git a/frontend/src/metabase/visualizations/components/FunnelBar.jsx b/frontend/src/metabase/visualizations/components/FunnelBar.jsx
index 79454994387531948a582ba368aa0fb591bb7687..1028592b93793e6032836dd598f2569d8dab1b0e 100644
--- a/frontend/src/metabase/visualizations/components/FunnelBar.jsx
+++ b/frontend/src/metabase/visualizations/components/FunnelBar.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import BarChart from "metabase/visualizations/visualizations/BarChart.jsx";
 
diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
index 2f793f8d1949ac20073e631854455fb7cb360d85..104b44a799aeec7554a0e098aaf5393e78b03361 100644
--- a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
+++ b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import cx from "classnames";
 import styles from "./FunnelNormal.css";
diff --git a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
index b958b5b4cd7890e6cb7bbd17cea5b557622fecf9..6099b9eeeec27cd5841fe10dda0396ca91dec170 100644
--- a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import { normal } from "metabase/lib/colors";
 
diff --git a/frontend/src/metabase/visualizations/components/LeafletMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMap.jsx
index e6e3d1bf109dfda4e0199a64e311acc6eaee701d..380d750eca5caa3411cc3277e909541e77ea92c1 100644
--- a/frontend/src/metabase/visualizations/components/LeafletMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletMap.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import MetabaseSettings from "metabase/lib/settings";
diff --git a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
index 7daf75bba13cadbd967286b64ddcf978cb495896..fabd69a39fdf6b55a855ab6ea5593271ca5a7028 100644
--- a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 import ReactDOM from "react-dom";
 
 import LeafletMap from "./LeafletMap.jsx";
diff --git a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
index da7b688e340f2a4792ad9d6143b5d620750030f3..9bcf98d86b3422035472c58b3a7969285904b916 100644
--- a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
@@ -1,4 +1,3 @@
-import React, { Component, PropTypes } from "react";
 
 import LeafletMap from "./LeafletMap.jsx";
 import L from "leaflet";
diff --git a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
index 8059a0b659cdf9f63ca053f339304d5777e0177d..b2f9b507242659772b8da09b59248494e46ec2c2 100644
--- a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
+++ b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { isSameSeries } from "metabase/visualizations/lib/utils";
 import d3 from "d3";
diff --git a/frontend/src/metabase/visualizations/components/LegendHeader.jsx b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
index 0d29843b595d4362aef8c5523adc295b86978fbe..d625cbd316b0b8a9d5b43be19681b68a66d02b8c 100644
--- a/frontend/src/metabase/visualizations/components/LegendHeader.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
@@ -1,11 +1,12 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import styles from "./Legend.css";
 
 import Icon from "metabase/components/Icon.jsx";
 import LegendItem from "./LegendItem.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 
@@ -65,7 +66,7 @@ export default class LegendHeader extends Component {
                         key={index}
                         title={s.card.name}
                         description={description}
-                        href={linkToCard && s.card.id && Urls.card(s.card.id)}
+                        href={linkToCard && s.card.id && Urls.question(s.card.id)}
                         color={colors[index % colors.length]}
                         showDot={showDots}
                         showTitle={showTitles}
diff --git a/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx b/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx
index 2e1d85296e25d47270225fe85245ca818b86c283..10600a508438ebd76db5a0329210b1ce1d1cb77b 100644
--- a/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./Legend.css";
 
diff --git a/frontend/src/metabase/visualizations/components/LegendItem.jsx b/frontend/src/metabase/visualizations/components/LegendItem.jsx
index 9a594e4317aee5a9370cfdea9afccbbe16dae67d..f7a59666cd9906591a723ed40cd910c3a571ca7a 100644
--- a/frontend/src/metabase/visualizations/components/LegendItem.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendItem.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
diff --git a/frontend/src/metabase/visualizations/components/LegendVertical.jsx b/frontend/src/metabase/visualizations/components/LegendVertical.jsx
index 1f78062295f24d22472abf0520a756eb2941b127..4d0cf771e2a966aa3f541546e10a1a69fd88aaf8 100644
--- a/frontend/src/metabase/visualizations/components/LegendVertical.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendVertical.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./Legend.css";
 
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
index e2716e6f90dd953c56e7bee892c527d16aa3366b..07bcaa255b0b28fc887723c29a5db3a7077bbdb2 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 import CardRenderer from "./CardRenderer.jsx";
 import LegendHeader from "./LegendHeader.jsx";
diff --git a/frontend/src/metabase/visualizations/components/PinMap.jsx b/frontend/src/metabase/visualizations/components/PinMap.jsx
index 596864c978aec3be5776947a4373acafef1068cc..ce57b2a3263bdbc60a9ca75797ea19f57bce7d23 100644
--- a/frontend/src/metabase/visualizations/components/PinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/PinMap.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata";
 import { LatitudeLongitudeError } from "metabase/visualizations/lib/errors";
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 4e33e5d28b67367582839eca374a08741290b841..af8a77121c4f6440c91aef961e29ac1fc542d087 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import "./TableInteractive.css";
diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx
index d2f5081be36f05185dd1b424540606134d3bdc47..db4d176f501c73b5a477d8ee7c35ce728e885c94 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import styles from "./Table.css";
 
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index 404daf44cd669f772dc44af176485e21fee053fe..bfc935a93c8a4d2eb64c4c7244af5d42c5f434a1 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -1,6 +1,6 @@
 /* @flow weak */
 
-import React, { Component, PropTypes, Element } from "react";
+import React, { Component, Element } from "react";
 
 import ExplicitSize from "metabase/components/ExplicitSize.jsx";
 import LegendHeader from "metabase/visualizations/components/LegendHeader.jsx";
@@ -15,13 +15,14 @@ import { duration, formatNumber } from "metabase/lib/formatting";
 import { getVisualizationTransformed } from "metabase/visualizations";
 import { getSettings } from "metabase/visualizations/lib/settings";
 import { isSameSeries } from "metabase/visualizations/lib/utils";
-import Utils from "metabase/lib/utils";
 
+import Utils from "metabase/lib/utils";
+import { datasetContainsNoResults } from "metabase/lib/dataset";
 import { getModeDrills } from "metabase/qb/lib/modes"
 
 import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
 
-import { assoc, getIn, setIn } from "icepick";
+import { assoc, setIn } from "icepick";
 import _ from "underscore";
 import cx from "classnames";
 
@@ -273,7 +274,8 @@ export default class Visualization extends Component<*, Props, State> {
         }
 
         if (!error) {
-            noResults = getIn(series, [0, "data", "rows", "length"]) === 0;
+            // $FlowFixMe
+            noResults = series[0] && series[0].data && datasetContainsNoResults(series[0].data);
         }
 
         let extra = (
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx
index 2a2731d01a5f75c9acaac6b3a1efc1eab116c7f4..1cbd1d953792156272f9fecf783c70e6be3bba76 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import ColorPicker from "metabase/components/ColorPicker";
 
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx
index 740e52a670fe48a8c43a12c4283f0398e0c03696..ff7d45d05ea2087d6ec0cc17df17deaf4f04e347 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import ChartSettingColorPicker from "./ChartSettingColorPicker.jsx";
 
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
index 2c7e9896d4e543749099ae6338367e71e13db6a1..32b6a0c6cc4a339df7a9bd5294da23d70e28756d 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Icon from "metabase/components/Icon";
 import cx from "classnames";
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
index 7e6ea5cc982dc8d9d68ae9788636d9212a71a23a..09463240b7e5ad01fb497df7bda19b2833540a93 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import ChartSettingFieldPicker from "./ChartSettingFieldPicker.jsx";
 
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx
index ecd0cbacdc632be6d37d292eb8b070305f5a8dfa..1f1fcfae14e1ea092a185c3a732f58334a948cfa 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 const ChartSettingInput = ({ value, onChange }) =>
     <input
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx
index 76a3dc4f57eecf26146d066d450b08864bb4988a..9341f85c8423a90722840fda1523a7d2485b815f 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import cx from "classnames";
 
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
index f72f67e7aff88627b6862e70930b12414e4238d2..4c9df5db752a42dd18ccd311e3c0ad5f014318c7 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import CheckBox from "metabase/components/CheckBox.jsx";
 import Icon from "metabase/components/Icon.jsx";
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx
index 0f137687605dafb25c609a88ca4fb7e5d03c0ab6..f935c2c623e5f95537d5270efe4c99209e342d79 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Radio from "metabase/components/Radio.jsx";
 
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx
index b8a2875d4fcf9d591e1caf2860f7cb5fad5d1378..cabe7746d865de7154e347e15eaa6efd6c173063 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Select from "metabase/components/Select.jsx";
 
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx
index b3c5c073842e2d137d526bbdb79c2dcdc80b15c8..d0d7f6680d6a26c08a511bb162dfd75e8c229d69 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx
@@ -1,4 +1,4 @@
-import React, { Component, PropTypes } from "react";
+import React from "react";
 
 import Toggle from "metabase/components/Toggle.jsx";
 
diff --git a/frontend/src/metabase/visualizations/index.js b/frontend/src/metabase/visualizations/index.js
index 2e9780978050ce64be8c86797c60c999dcffb99b..0297c20a1cc69a0b886380efc8d5fdb3d7967ea5 100644
--- a/frontend/src/metabase/visualizations/index.js
+++ b/frontend/src/metabase/visualizations/index.js
@@ -1,7 +1,5 @@
 /* @flow weak */
 
-import { Component } from "react";
-
 import Scalar      from "./visualizations/Scalar.jsx";
 import Progress    from "./visualizations/Progress.jsx";
 import Table       from "./visualizations/Table.jsx";
diff --git a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
index 4732041c65bd3c9ba989cca1b9192bce244fe030..6043514557e20b730e858285f8f5da3c933b0ebc 100644
--- a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
@@ -1,6 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
 
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { areaRenderer } from "../lib/LineAreaBarRenderer";
diff --git a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
index 3df88ea438cfc3d5a3e159916e1dfa8347a08637..aa2b8bf74d93325e5929a4b2484331c37277dc61 100644
--- a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
@@ -1,6 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
 
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { barRenderer } from "../lib/LineAreaBarRenderer";
diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
index 4fd4fb8ef028bf4a1527415d56701387613a9fb5..ab15739464b8828ff35ce45aeef0c7671bc0a3c5 100644
--- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
 
@@ -51,14 +51,14 @@ export default class Funnel extends Component<*, VisualizationProps, *> {
         "funnel.dimension": {
             section: "Data",
             title: "Step",
-            ...dimensionSetting("pie.dimension"),
+            ...dimensionSetting("funnel.dimension"),
             dashboard: false,
             useRawSeries: true,
         },
         "funnel.metric": {
             section: "Data",
             title: "Measure",
-            ...metricSetting("pie.metric"),
+            ...metricSetting("funnel.metric"),
             dashboard: false,
             useRawSeries: true,
         },
diff --git a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
index 922dcb00dd9ef48b723075b5b614436250f5c8c9..9b981abc43242dd86c0e7172f779db69e9e62f63 100644
--- a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
@@ -1,6 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
 
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { lineRenderer } from "../lib/LineAreaBarRenderer";
diff --git a/frontend/src/metabase/visualizations/visualizations/Map.jsx b/frontend/src/metabase/visualizations/visualizations/Map.jsx
index d552cc87a7f73032b44ff851b3bf9b78b6ae5d9e..cd65132915a7cb0863a0df028997e4994fc99110 100644
--- a/frontend/src/metabase/visualizations/visualizations/Map.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Map.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import ChoroplethMap from "../components/ChoroplethMap.jsx";
 import PinMap from "../components/PinMap.jsx";
diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
index b922e95cb76714c4bd966056a2e65ff4b5922392..56e92be176230a0cbf49f72af1508f25d0397c57 100644
--- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./PieChart.css";
 
@@ -184,8 +184,8 @@ export default class PieChart extends Component<*, Props, *> {
 
         let value, title;
         if (hovered && hovered.index != null && slices[hovered.index] !== otherSlice) {
-            title = slices[hovered.index].key;
-            value = slices[hovered.index].value;
+            title = formatDimension(slices[hovered.index].key);
+            value = formatMetric(slices[hovered.index].value);
         } else {
             title = "Total";
             value = formatMetric(total);
diff --git a/frontend/src/metabase/visualizations/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
index 1189f5d1afcf797b275fcd31bcb3d34e8f33c9cf..1cf8fa459e3724b283bb64da1805121b0af692ab 100644
--- a/frontend/src/metabase/visualizations/visualizations/Progress.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 import { formatValue } from "metabase/lib/formatting";
diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
index c35274410df90e46718d856692f82e903fabd0e0..819123485d8e62f20621cdaa63a437a188991b4c 100644
--- a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
@@ -1,6 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
 
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { rowRenderer } from "../lib/LineAreaBarRenderer";
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
index 33dda37c911ae1e6c651788909cefd9e5ad26151..f55135518c34f78701d54cab1e5d7b273b2b036f 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 import { Link } from "react-router";
 import styles from "./Scalar.css";
 
@@ -8,7 +8,7 @@ import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 import { formatValue } from "metabase/lib/formatting";
 import { TYPE } from "metabase/lib/types";
 import { isNumber } from "metabase/lib/schema_metadata";
@@ -195,7 +195,7 @@ export default class Scalar extends Component<*, VisualizationProps, *> {
                 <div className={styles.Title + " flex align-center"}>
                     <Ellipsified tooltip={card.name}>
                         { linkToCard ?
-                          <Link to={Urls.card(card.id)} className="no-decoration fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</Link>
+                          <Link to={Urls.question(card.id)} className="no-decoration fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</Link>
                           :
                           <span className="fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</span>
                         }
diff --git a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
index 8a5c9ac2dc1eb713966092f6a3319c9a6f5daaea..034929f09cd561d47692bebb9ab2d877d4bcbc3f 100644
--- a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
@@ -1,6 +1,5 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
 
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { scatterRenderer } from "../lib/LineAreaBarRenderer";
diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.jsx
index bd9477488b93f34145278e3f5ddb0e23f4e34bfc..d45ac9c86111bb8a0529872b0f51f694db5501af 100644
--- a/frontend/src/metabase/visualizations/visualizations/Table.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Table.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-import React, { Component, PropTypes } from "react";
+import React, { Component } from "react";
 
 import TableInteractive from "../components/TableInteractive.jsx";
 import TableSimple from "../components/TableSimple.jsx";
diff --git a/frontend/test/e2e/auth/login.spec.js b/frontend/test/e2e/auth/login.spec.js
index d67f0476d4aff0f55317f1b3a5c71743fda8e77e..fe191cebf9c5107762286b3346a5890b20aed86a 100644
--- a/frontend/test/e2e/auth/login.spec.js
+++ b/frontend/test/e2e/auth/login.spec.js
@@ -55,8 +55,8 @@ describeE2E("auth/login", () => {
         });
 
         it("loads the qb", async () => {
-            await driver.get(`${server.host}/q#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
-            await waitForUrl(driver, `${server.host}/q#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
+            await driver.get(`${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
+            await waitForUrl(driver, `${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
             await screenshot(driver, "screenshots/qb.png");
         });
     });
diff --git a/frontend/test/e2e/query_builder/query_builder.spec.js b/frontend/test/e2e/query_builder/query_builder.spec.js
index 6e4106bd8722b4ed842020f59468a80d715e3e2a..9eb6aa8d69d39a6dc0d9d572ca0f2113db7db8ee 100644
--- a/frontend/test/e2e/query_builder/query_builder.spec.js
+++ b/frontend/test/e2e/query_builder/query_builder.spec.js
@@ -15,7 +15,7 @@ describeE2E("query_builder", () => {
     describe("tables", () => {
         it("should allow users to create pivot tables", async () => {
             // load the query builder and screenshot blank
-            await d.get("/q");
+            await d.get("/question");
             await d.screenshot("screenshots/qb-initial.png");
 
             // pick the orders table (assumes database is already selected, i.e. there's only 1 database)
@@ -56,7 +56,7 @@ describeE2E("query_builder", () => {
 
     describe("charts", () => {
         xit("should allow users to create line charts", async () => {
-            await d.get("/q");
+            await d.get("/question");
 
             // select orders table
             await d.select("#TablePicker .List-item:first-child>a").wait().click();
@@ -106,7 +106,7 @@ describeE2E("query_builder", () => {
 
         xit("should allow users to create bar charts", async () => {
             // load line chart
-            await d.get("/card/2");
+            await d.get("/question/2");
 
             // dismiss saved questions modal
             await d.select(".Modal .Button.Button--primary").wait().click();
diff --git a/frontend/test/e2e/query_builder/tutorial.spec.js b/frontend/test/e2e/query_builder/tutorial.spec.js
index 8b6b31f589a2becb8170a0bac563025c3ad08501..9298adaf0a18211a64e7647e82286bf14ddf5504 100644
--- a/frontend/test/e2e/query_builder/tutorial.spec.js
+++ b/frontend/test/e2e/query_builder/tutorial.spec.js
@@ -24,7 +24,7 @@ describeE2E("tutorial", () => {
         await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
         await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
 
-        await waitForUrl(driver, `${server.host}/q`);
+        await waitForUrl(driver, `${server.host}/question`);
         await waitForElement(driver, ".Modal .Button.Button--primary");
         await screenshot(driver, "screenshots/setup-tutorial-qb.png");
         await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
diff --git a/package.json b/package.json
index 4473174e5dfd9341a9a811ba925c23e4df144749..b850a4d0ebecad6003d4242671a34a0a3eab43ef 100644
--- a/package.json
+++ b/package.json
@@ -34,14 +34,15 @@
     "normalizr": "^3.0.2",
     "number-to-locale-string": "^1.0.1",
     "password-generator": "^2.0.1",
-    "react": "^15.2.1",
+    "prop-types": "^15.5.4",
+    "react": "15.5.0",
     "react-addons-css-transition-group": "^15.2.1",
     "react-addons-perf": "^15.2.1",
     "react-addons-shallow-compare": "^15.2.1",
     "react-ansi-style": "^1.0.0",
     "react-collapse": "^2.3.3",
     "react-copy-to-clipboard": "^4.2.3",
-    "react-dom": "^15.2.1",
+    "react-dom": "15.5.0",
     "react-draggable": "^2.2.3",
     "react-height": "^2.1.1",
     "react-motion": "^0.4.5",
@@ -103,6 +104,7 @@
     "flow-bin": "^0.37.4",
     "fs-promise": "^1.0.0",
     "glob": "^7.1.1",
+    "html-webpack-harddisk-plugin": "^0.1.0",
     "html-webpack-plugin": "^2.14.0",
     "husky": "^0.13.2",
     "image-diff": "^1.6.3",
@@ -154,7 +156,7 @@
     "test-e2e-sauce": "USE_SAUCE=true yarn run test-e2e",
     "build": "webpack --bail",
     "build-watch": "webpack --watch",
-    "build-hot": "NODE_ENV=hot webpack --bail && NODE_ENV=hot webpack-dev-server --progress",
+    "build-hot": "NODE_ENV=hot webpack-dev-server --progress",
     "start": "yarn run build && lein ring server",
     "storybook": "start-storybook -p 9001",
     "precommit": "lint-staged",
diff --git a/project.clj b/project.clj
index 9177e9125445f387d2b1e67943478345bd0c1026..f972daf60dc0175ebe9ae6ca40f91f24c996eab1 100644
--- a/project.clj
+++ b/project.clj
@@ -12,7 +12,7 @@
             "profile" ["with-profile" "+profile" "run" "profile"]
             "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"]}
   :dependencies [[org.clojure/clojure "1.8.0"]
-                 [org.clojure/core.async "0.2.395"]
+                 [org.clojure/core.async "0.3.442"]
                  [org.clojure/core.match "0.3.0-alpha4"]              ; optimized pattern matching library for Clojure
                  [org.clojure/core.memoize "0.5.9"]                   ; needed by core.match; has useful FIFO, LRU, etc. caching mechanisms
                  [org.clojure/data.csv "0.1.3"]                       ; CSV parsing / generation
@@ -25,7 +25,7 @@
                   :exclusions [org.clojure/clojure
                                org.clojure/clojurescript]]            ; fixed length queue implementation, used in log buffering
                  [amalloy/ring-gzip-middleware "0.1.3"]               ; Ring middleware to GZIP responses if client can handle it
-                 [aleph "0.4.1"]                                      ; Async HTTP library; WebSockets
+                 [aleph "0.4.3"]                                      ; Async HTTP library; WebSockets
                  [buddy/buddy-core "1.2.0"]                           ; various cryptograhpic functions
                  [buddy/buddy-sign "1.5.0"]                           ; JSON Web Tokens; High-Level message signing library
                  [cheshire "5.7.0"]                                   ; fast JSON encoding (used by Ring JSON middleware)
@@ -43,13 +43,14 @@
                                ring/ring-core]]
                  [com.draines/postal "2.0.2"]                         ; SMTP library
                  [com.google.apis/google-api-services-analytics       ; Google Analytics Java Client Library
-                  "v3-rev136-1.22.0"]
+                  "v3-rev139-1.22.0"]
                  [com.google.apis/google-api-services-bigquery        ; Google BigQuery Java Client Library
-                  "v2-rev334-1.22.0"]
-                 [com.h2database/h2 "1.4.193"]                        ; embedded SQL database
+                  "v2-rev342-1.22.0"]
+                 [com.h2database/h2 "1.4.194"]                        ; embedded SQL database
                  [com.mattbertolini/liquibase-slf4j "2.0.0"]          ; Java Migrations lib
                  [com.mchange/c3p0 "0.9.5.2"]                         ; connection pooling library
                  [com.novemberain/monger "3.1.0"]                     ; MongoDB Driver
+                 [com.taoensso/nippy "2.13.0"]                        ; Fast serialization (i.e., GZIP) library for Clojure
                  [compojure "1.5.2"]                                  ; HTTP Routing library built on Ring
                  [crypto-random "1.2.0"]                              ; library for generating cryptographically secure random bytes and strings
                  [environ "1.1.0"]                                    ; easy environment management
@@ -67,20 +68,20 @@
                   :exclusions [org.slf4j/slf4j-api]]
                  [net.sourceforge.jtds/jtds "1.3.1"]                  ; Open Source SQL Server driver
                  [org.liquibase/liquibase-core "3.5.3"]               ; migration management (Java lib)
-                 [org.slf4j/slf4j-log4j12 "1.7.22"]                   ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
-                 [org.yaml/snakeyaml "1.17"]                          ; YAML parser (required by liquibase)
-                 [org.xerial/sqlite-jdbc "3.8.11.2"]                  ; SQLite driver !!! DO NOT UPGRADE THIS UNTIL UPSTREAM BUG IS FIXED -- SEE https://github.com/metabase/metabase/issues/3753 !!!
+                 [org.slf4j/slf4j-log4j12 "1.7.25"]                   ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
+                 [org.yaml/snakeyaml "1.18"]                          ; YAML parser (required by liquibase)
+                 [org.xerial/sqlite-jdbc "3.16.1"]                    ; SQLite driver
                  [postgresql "9.3-1102.jdbc41"]                       ; Postgres driver
                  [io.crate/crate-jdbc "2.1.6"]                        ; Crate JDBC driver
-                 [prismatic/schema "1.1.3"]                           ; Data schema declaration and validation library
+                 [prismatic/schema "1.1.5"]                           ; Data schema declaration and validation library
                  [ring/ring-jetty-adapter "1.5.1"]                    ; Ring adapter using Jetty webserver (used to run a Ring server for unit tests)
                  [ring/ring-json "0.4.0"]                             ; Ring middleware for reading/writing JSON automatically
                  [stencil "0.5.0"]                                    ; Mustache templates for Clojure
                  [toucan "1.0.2"                                      ; Model layer, hydration, and DB utilities
                   :exclusions [honeysql]]]
   :repositories [["bintray" "https://dl.bintray.com/crate/crate"]]    ; Repo for Crate JDBC driver
-  :plugins [[lein-environ "1.0.3"]                                    ; easy access to environment variables
-            [lein-ring "0.9.7"                                        ; start the HTTP server with 'lein ring server'
+  :plugins [[lein-environ "1.1.0"]                                    ; easy access to environment variables
+            [lein-ring "0.11.0"                                       ; start the HTTP server with 'lein ring server'
              :exclusions [org.clojure/clojure]]]                      ; TODO - should this be a dev dependency ?
   :main ^:skip-aot metabase.core
   :manifest {"Liquibase-Package" "liquibase.change,liquibase.changelog,liquibase.database,liquibase.parser,liquibase.precondition,liquibase.datatype,liquibase.serializer,liquibase.sqlgenerator,liquibase.executor,liquibase.snapshot,liquibase.logging,liquibase.diff,liquibase.structure,liquibase.structurecompare,liquibase.lockservice,liquibase.sdk,liquibase.ext"}
@@ -106,13 +107,12 @@
   :docstring-checker {:include [#"^metabase"]
                       :exclude [#"test"
                                 #"^metabase\.http-client$"]}
-  :profiles {:dev {:dependencies [[org.clojure/tools.nrepl "0.2.12"]  ; REPL <3
-                                  [expectations "2.1.9"]              ; unit tests
+  :profiles {:dev {:dependencies [[expectations "2.1.9"]              ; unit tests
                                   [ring/ring-mock "0.3.0"]]           ; Library to create mock Ring requests for unit tests
                    :plugins [[docstring-checker "1.0.0"]              ; Check that all public vars have docstrings. Run with 'lein docstring-checker'
                              [jonase/eastwood "0.2.3"
                               :exclusions [org.clojure/clojure]]      ; Linting
-                             [lein-bikeshed "0.3.0"]                  ; Linting
+                             [lein-bikeshed "0.4.1"]                  ; Linting
                              [lein-expectations "0.0.8"]              ; run unit tests with 'lein expectations'
                              [lein-instant-cheatsheet "2.2.1"         ; use awesome instant cheatsheet created by yours truly w/ 'lein instant-cheatsheet'
                               :exclusions [org.clojure/clojure
diff --git a/reset_password/metabase/reset_password/core.clj b/reset_password/metabase/reset_password/core.clj
index f963a49e8dc88550ed3788842ea355119e7d5a75..510adf2a331102750cd1a5a8ccc3f54782d3249a 100644
--- a/reset_password/metabase/reset_password/core.clj
+++ b/reset_password/metabase/reset_password/core.clj
@@ -14,10 +14,10 @@
 (defn -main
   [email-address]
   (mdb/setup-db!)
-  (println (format "Resetting password for %s..." email-address))
+  (printf "Resetting password for %s...\n" email-address)
   (try
-    (println (format "OK [[[%s]]]" (set-reset-token! email-address)))
+    (printf "OK [[[%s]]]\n" (set-reset-token! email-address))
     (System/exit 0)
     (catch Throwable e
-      (println (format "FAIL [[[%s]]]" (.getMessage e)))
+      (printf "FAIL [[[%s]]]\n" (.getMessage e))
       (System/exit -1))))
diff --git a/resources/frontend_client/index_template.html b/resources/frontend_client/index_template.html
index 5086fc555cda70e69ef449c369df68184726e109..573608b6bd9945517b89250107362f6c3607076a 100644
--- a/resources/frontend_client/index_template.html
+++ b/resources/frontend_client/index_template.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en" ng-app="metabase" class="no-js">
+<html lang="en">
     <head>
         <meta charset="utf-8" />
         <meta http-equiv="X-UA-Compatible" content="IE=edge" />
@@ -12,50 +12,48 @@
         <title>Metabase</title>
 
         <script type="text/javascript">
-         window.MetabaseBootstrap = {{{bootstrap_json}}};
+            window.MetabaseBootstrap = {{{bootstrap_json}}};
         </script>
     </head>
 
     <body>
-        <div id="root" />
-        <div style="display: none;" ng-controller="Metabase" ng-view />
-    </body>
+        <div id="root"></div>
+
+        <script type="text/javascript">
+            // Load scripts asyncronously after the page has finished loading
+            (function () {
+                function loadScript(src, onload) {
+                    var script = document.createElement('script');
+                    script.type  = "text/javascript";
+                    script.async = true;
+                    script.src   = src;
+                    if (onload) script.onload = onload;
+                    document.body.appendChild(script);
+                }
+                loadScript('https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js', function () {
+                    WebFont.load({ google: { families: ["Lato:n3,n4,n9"] } });
+                });
+                var googleAuthClientID = window.MetabaseBootstrap.google_auth_client_id;
+                if (googleAuthClientID) {
+                    loadScript('https://apis.google.com/js/api:client.js');
+                }
+            })();
+        </script>
 
-    <script type="text/javascript">
-     // Load scripts asyncronously after the page has finished loading
-     (function () {
-         function loadScript(src, onload) {
-             var script = document.createElement('script');
-             script.type  = "text/javascript";
-             script.async = true;
-             script.src   = src;
-             if (onload) script.onload = onload;
-             document.body.appendChild(script);
-         }
-         loadScript('https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js', function () {
-             WebFont.load({ google: { families: ["Lato:n3,n4,n9"] } });
-         });
-
-         var googleAuthClientID = window.MetabaseBootstrap.google_auth_client_id;
-         if (googleAuthClientID) {
-             loadScript('https://apis.google.com/js/api:client.js');
-         }
-     })();
-    </script>
-
-    <script>
-    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
-
-    // if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview
-    const tracking = window.MetabaseBootstrap.anon_tracking_enabled;
-    const ga_code = window.MetabaseBootstrap.ga_code;
-    if (!tracking) {
-        window['ga-disable-'+ga_code] = true;
-    }
-
-    ga('create', ga_code, 'auto');
-    </script>
+        <script type="text/javascript">
+            (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+            })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+
+            // if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview
+            const tracking = window.MetabaseBootstrap.anon_tracking_enabled;
+            const ga_code = window.MetabaseBootstrap.ga_code;
+            if (!tracking) {
+                window['ga-disable-'+ga_code] = true;
+            }
+
+            ga('create', ga_code, 'auto');
+        </script>
+    </body>
 </html>
diff --git a/resources/migrations/052_add_query_cache_table.yaml b/resources/migrations/052_add_query_cache_table.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e21e19f5047f4a544217a8faf4d822d624cae3b0
--- /dev/null
+++ b/resources/migrations/052_add_query_cache_table.yaml
@@ -0,0 +1,49 @@
+databaseChangeLog:
+  - property:
+        name: blob.type
+        value: blob
+        dbms: mysql,h2
+  - property:
+      name: blob.type
+      value: bytea
+      dbms: postgresql
+  - changeSet:
+      id: 52
+      author: camsaul
+      changes:
+        - createTable:
+            tableName: query_cache
+            remarks: 'Cached results of queries are stored here when using the DB-based query cache.'
+            columns:
+              - column:
+                  name: query_hash
+                  type: binary(32)
+                  remarks: 'The hash of the query dictionary. (This is a 256-bit SHA3 hash of the query dict).'
+                  constraints:
+                    primaryKey: true
+                    nullable: false
+              - column:
+                  name: updated_at
+                  type: datetime
+                  remarks: 'The timestamp of when these query results were last refreshed.'
+                  constraints:
+                    nullable: false
+              - column:
+                  name: results
+                  type: ${blob.type}
+                  remarks: 'Cached, compressed results of running the query with the given hash.'
+                  constraints:
+                    nullable: false
+        - createIndex:
+            tableName: query_cache
+            indexName: idx_query_cache_updated_at
+            columns:
+              column:
+                name: updated_at
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  name: cache_ttl
+                  type: int
+                  remarks: 'The maximum time, in seconds, to return cached results for this Card rather than running a new query.'
diff --git a/resources/migrations/053_add_query_table.yaml b/resources/migrations/053_add_query_table.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..772de999cb50a8a43200f4f043ff3034ed93d425
--- /dev/null
+++ b/resources/migrations/053_add_query_table.yaml
@@ -0,0 +1,22 @@
+databaseChangeLog:
+  - changeSet:
+      id: 53
+      author: camsaul
+      changes:
+        - createTable:
+            tableName: query
+            remarks: 'Information (such as average execution time) for different queries that have been previously ran.'
+            columns:
+              - column:
+                  name: query_hash
+                  type: binary(32)
+                  remarks: 'The hash of the query dictionary. (This is a 256-bit SHA3 hash of the query dict.)'
+                  constraints:
+                    primaryKey: true
+                    nullable: false
+              - column:
+                  name: average_execution_time
+                  type: int
+                  remarks: 'Average execution time for the query, round to nearest number of milliseconds. This is updated as a rolling average.'
+                  constraints:
+                    nullable: false
diff --git a/sample_dataset/metabase/sample_dataset/generate.clj b/sample_dataset/metabase/sample_dataset/generate.clj
index 3807dbcc1427aa73d7334a964379adae8c0a4a86..50bd431ce21ea4db945264236063c992fae29c91 100644
--- a/sample_dataset/metabase/sample_dataset/generate.clj
+++ b/sample_dataset/metabase/sample_dataset/generate.clj
@@ -253,7 +253,7 @@
           (= (count (:products %)) products)
           (every? keyword? (keys %))
           (every? sequential? (vals %))]}
-  (println (format "Generating random data: %d people, %d products..." people products))
+  (printf "Generating random data: %d people, %d products...\n" people products)
   (let [products (mapv product-add-reviews (create-randoms products random-product))
         people   (mapv (partial person-add-orders products) (create-randoms people random-person))]
     {:people   (mapv #(dissoc % :orders) people)
@@ -416,6 +416,6 @@
 
 (defn -main [& [filename]]
   (let [filename (or filename sample-dataset-filename)]
-    (println (format "Writing sample dataset to %s..." filename))
+    (printf "Writing sample dataset to %s...\n" filename)
     (create-h2-db filename)
     (System/exit 0)))
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index 3c3b09c511463c7a0e5d6d112de9f85b850a723f..e27c8c855ce6dcc1e2143a40bf4663287d34e84f 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -1,7 +1,9 @@
 (ns metabase.api.card
   (:require [clojure.data :as data]
+            [clojure.tools.logging :as log]
             [cheshire.core :as json]
             [compojure.core :refer [GET POST DELETE PUT]]
+            [ring.util.codec :as codec]
             [schema.core :as s]
             (toucan [db :as db]
                     [hydrate :refer [hydrate]])
@@ -18,10 +20,14 @@
                              [interface :as mi]
                              [label :refer [Label]]
                              [permissions :as perms]
+                             [query :as query]
                              [table :refer [Table]]
                              [view-log :refer [ViewLog]])
-            (metabase [query-processor :as qp]
-                      [util :as u])
+            [metabase.public-settings :as public-settings]
+            [metabase.query-processor :as qp]
+            [metabase.query-processor.middleware.cache :as cache]
+            [metabase.query-processor.util :as qputil]
+            [metabase.util :as u]
             [metabase.util.schema :as su])
   (:import java.util.UUID))
 
@@ -128,8 +134,18 @@
 (defn- ^:deprecated card-has-label? [label-slug card]
   (contains? (set (map :slug (:labels card))) label-slug))
 
+(defn- collection-slug->id [collection-slug]
+  (when (seq collection-slug)
+    ;; special characters in the slugs are always URL-encoded when stored in the DB, e.g.
+    ;; "Obsługa klienta" becomes "obs%C5%82uga_klienta". But for some weird reason sometimes the slug is passed in like
+    ;; "obsługa_klientaa" (not URL-encoded) so go ahead and URL-encode the input as well so we can match either case
+    (check-404 (db/select-one-id Collection
+                 {:where [:or [:= :slug collection-slug]
+                          [:= :slug (codec/url-encode collection-slug)]]}))))
+
 ;; TODO - do we need to hydrate the cards' collections as well?
-(defn- cards-for-filter-option [filter-option model-id label collection]
+(defn- cards-for-filter-option [filter-option model-id label collection-slug]
+  (println "collection-slug:" collection-slug) ; NOCOMMIT
   (let [cards (-> ((filter-option->fn (or filter-option :all)) model-id)
                   (hydrate :creator :collection)
                   hydrate-labels
@@ -137,11 +153,10 @@
     ;; Since labels and collections are hydrated in Clojure-land we need to wait until this point to apply label/collection filtering if applicable
     ;; COLLECTION can optionally be an empty string which is used to repre
     (filter (cond
-              collection  (let [collection-id (when (seq collection)
-                                                (check-404 (db/select-one-id Collection :slug collection)))]
-                            (comp (partial = collection-id) :collection_id))
-              (seq label) (partial card-has-label? label)
-              :else       identity)
+              collection-slug (let [collection-id (collection-slug->id collection-slug)]
+                                (comp (partial = collection-id) :collection_id))
+              (seq label)     (partial card-has-label? label)
+              :else           identity)
             cards)))
 
 
@@ -227,7 +242,7 @@
   {name                   (s/maybe su/NonBlankString)
    dataset_query          (s/maybe su/Map)
    display                (s/maybe su/NonBlankString)
-   description            (s/maybe su/NonBlankString)
+   description            (s/maybe s/Str)
    visualization_settings (s/maybe su/Map)
    archived               (s/maybe s/Bool)
    enable_embedding       (s/maybe s/Bool)
@@ -254,12 +269,17 @@
       (check-superuser))
     ;; ok, now save the Card
     (db/update! Card id
-      (merge (when (contains? body :collection_id)
-               {:collection_id collection_id})
-             (into {} (for [k     [:dataset_query :description :display :name :visualization_settings :archived :enable_embedding :embedding_params]
-                            :let  [v (k body)]
-                            :when (not (nil? v))]
-                        {k v}))))
+      (merge
+       ;; `collection_id` and `description` can be `nil` (in order to unset them)
+       (when (contains? body :collection_id)
+         {:collection_id collection_id})
+       (when (contains? body :description)
+         {:description description})
+       ;; other values should only be modified if they're passed in as non-nil
+       (into {} (for [k     [:dataset_query :display :name :visualization_settings :archived :enable_embedding :embedding_params]
+                      :let  [v (k body)]
+                      :when (not (nil? v))]
+                  {k v}))))
     (let [event (cond
                   ;; card was archived
                   (and archived
@@ -354,6 +374,26 @@
 
 ;;; ------------------------------------------------------------ Running a Query ------------------------------------------------------------
 
+(defn- query-magic-ttl
+  "Compute a 'magic' cache TTL time (in seconds) for QUERY by multipling its historic average execution times by the `query-caching-ttl-ratio`.
+   If the TTL is less than a second, this returns `nil` (i.e., the cache should not be utilized.)"
+  [query]
+  (when-let [average-duration (query/average-execution-time-ms (qputil/query-hash query))]
+    (let [ttl-seconds (Math/round (float (/ (* average-duration (public-settings/query-caching-ttl-ratio))
+                                            1000.0)))]
+      (when-not (zero? ttl-seconds)
+        (log/info (format "Question's average execution duration is %d ms; using 'magic' TTL of %d seconds" average-duration ttl-seconds) (u/emoji "💾"))
+        ttl-seconds))))
+
+(defn- query-for-card [card parameters constraints]
+  (let [query (assoc (:dataset_query card)
+                :constraints constraints
+                :parameters  parameters)
+        ttl   (when (public-settings/enable-query-caching)
+                (or (:cache_ttl card)
+                    (query-magic-ttl query)))]
+    (assoc query :cache_ttl ttl)))
+
 (defn run-query-for-card
   "Run the query for Card with PARAMETERS and CONSTRAINTS, and return results in the usual format."
   {:style/indent 1}
@@ -362,9 +402,7 @@
                      context     :question}}]
   {:pre [(u/maybe? sequential? parameters)]}
   (let [card    (read-check Card card-id)
-        query   (assoc (:dataset_query card)
-                  :parameters  parameters
-                  :constraints constraints)
+        query   (query-for-card card parameters constraints)
         options {:executed-by  *current-user-id*
                  :context      context
                  :card-id      card-id
@@ -374,26 +412,30 @@
 
 (defendpoint POST "/:card-id/query"
   "Run the query associated with a Card."
-  [card-id, :as {{:keys [parameters]} :body}]
-  (run-query-for-card card-id, :parameters parameters))
+  [card-id :as {{:keys [parameters ignore_cache], :or {ignore_cache false}} :body}]
+  {ignore_cache (s/maybe s/Bool)}
+  (binding [cache/*ignore-cached-results* ignore_cache]
+    (run-query-for-card card-id, :parameters parameters)))
 
 (defendpoint POST "/:card-id/query/csv"
   "Run the query associated with a Card, and return its results as CSV. Note that this expects the parameters as serialized JSON in the 'parameters' parameter"
   [card-id parameters]
   {parameters (s/maybe su/JSONString)}
-  (dataset-api/as-csv (run-query-for-card card-id
-                        :parameters  (json/parse-string parameters keyword)
-                        :constraints nil
-                        :context     :csv-download)))
+  (binding [cache/*ignore-cached-results* true]
+    (dataset-api/as-csv (run-query-for-card card-id
+                          :parameters  (json/parse-string parameters keyword)
+                          :constraints nil
+                          :context     :csv-download))))
 
 (defendpoint POST "/:card-id/query/json"
   "Run the query associated with a Card, and return its results as JSON. Note that this expects the parameters as serialized JSON in the 'parameters' parameter"
   [card-id parameters]
   {parameters (s/maybe su/JSONString)}
-  (dataset-api/as-json (run-query-for-card card-id
-                         :parameters  (json/parse-string parameters keyword)
-                         :constraints nil
-                         :context     :json-download)))
+  (binding [cache/*ignore-cached-results* true]
+    (dataset-api/as-json (run-query-for-card card-id
+                           :parameters  (json/parse-string parameters keyword)
+                           :constraints nil
+                           :context     :json-download))))
 
 
 ;;; ------------------------------------------------------------ Sharing is Caring ------------------------------------------------------------
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index d252c6542321c5023ded495ffe5342a03c1b2d5a..e965621b68f40904f7e45b873a7811c5828fbc40 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -73,7 +73,7 @@
    but to change the value of `enable_embedding` you must be a superuser."
   [id :as {{:keys [description name parameters caveats points_of_interest show_in_getting_started enable_embedding embedding_params], :as dashboard} :body}]
   {name                    (s/maybe su/NonBlankString)
-   description             (s/maybe su/NonBlankString)
+   description             (s/maybe s/Str)
    caveats                 (s/maybe su/NonBlankString)
    points_of_interest      (s/maybe su/NonBlankString)
    show_in_getting_started (s/maybe su/NonBlankString)
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index ddeaf0e370cf7040ede72a001eb480c11d3e61ca..0dae851c67f8b593f82f55715825106e9029bb05 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -8,6 +8,7 @@
                     [hydrate :refer [hydrate]])
             (metabase.models [card :refer [Card]]
                              [database :refer [Database]]
+                             [query :as query]
                              [query-execution :refer [QueryExecution]])
             [metabase.query-processor :as qp]
             [metabase.query-processor.util :as qputil]
@@ -41,8 +42,8 @@
   (read-check Database database)
   ;; try calculating the average for the query as it was given to us, otherwise with the default constraints if there's no data there.
   ;; if we still can't find relevant info, just default to 0
-  {:average (or (qputil/query-average-duration query)
-                (qputil/query-average-duration (assoc query :constraints default-query-constraints))
+  {:average (or (query/average-execution-time-ms (qputil/query-hash query))
+                (query/average-execution-time-ms (qputil/query-hash (assoc query :constraints default-query-constraints)))
                 0)})
 
 (defn as-csv
diff --git a/src/metabase/cmd/load_from_h2.clj b/src/metabase/cmd/load_from_h2.clj
index cd73db016935c026d12edb1c7664b014656df981..879b4e35ac043a33068fcadc2387b8b9c38370ec 100644
--- a/src/metabase/cmd/load_from_h2.clj
+++ b/src/metabase/cmd/load_from_h2.clj
@@ -46,7 +46,6 @@
                              [pulse-card :refer [PulseCard]]
                              [pulse-channel :refer [PulseChannel]]
                              [pulse-channel-recipient :refer [PulseChannelRecipient]]
-                             [query-execution :refer [QueryExecution]]
                              [raw-column :refer [RawColumn]]
                              [raw-table :refer [RawTable]]
                              [revision :refer [Revision]]
@@ -89,7 +88,6 @@
    DashboardCard
    DashboardCardSeries
    Activity
-   QueryExecution
    Pulse
    PulseCard
    PulseChannel
diff --git a/src/metabase/config.clj b/src/metabase/config.clj
index e5d45b1b6c7dab6b361c24b6554237862ea7bc29..ac7ff3fd221372021a4a8776af0136b0b9ad5fa1 100644
--- a/src/metabase/config.clj
+++ b/src/metabase/config.clj
@@ -29,7 +29,8 @@
    :mb-version-info-url "http://static.metabase.com/version-info.json"
    :max-session-age "20160"                     ; session length in minutes (14 days)
    :mb-colorize-logs "true"
-   :mb-emoji-in-logs "true"})
+   :mb-emoji-in-logs "true"
+   :mb-qp-cache-backend "db"})
 
 
 (defn config-str
diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj
index 8e4c41e194baa3c014da31a0f84141ef7332734b..6e9b011b647f2770f88ba82ed3b44dbb639201bb 100644
--- a/src/metabase/driver/bigquery.clj
+++ b/src/metabase/driver/bigquery.clj
@@ -77,7 +77,8 @@
 
 (defn- can-connect? [details-map]
   {:pre [(map? details-map)]}
-  (boolean (describe-database {:details details-map})))
+  ;; check whether we can connect by just fetching the first page of tables for the database. If that succeeds we're g2g
+  (boolean (list-tables {:details details-map})))
 
 
 (defn- ^Table get-table
@@ -358,13 +359,71 @@
 ;; ORDER BY [sad_toucan_incidents.incidents.timestamp] ASC
 ;; LIMIT 10
 
-(defn- field->identitfier [field]
+(defn- deduplicate-aliases
+  "Given a sequence of aliases, return a sequence where duplicate aliases have been appropriately suffixed.
+
+     (deduplicate-aliases [\"sum\" \"count\" \"sum\" \"avg\" \"sum\" \"min\"])
+     ;; -> [\"sum\" \"count\" \"sum_2\" \"avg\" \"sum_3\" \"min\"]"
+  [aliases]
+  (loop [acc [], alias->use-count {}, [alias & more, :as aliases] aliases]
+    (let [use-count (get alias->use-count alias)]
+      (cond
+        (empty? aliases) acc
+        (not alias)      (recur (conj acc alias) alias->use-count more)
+        (not use-count)  (recur (conj acc alias) (assoc alias->use-count alias 1) more)
+        :else            (let [new-count (inc use-count)
+                               new-alias (str alias "_" new-count)]
+                           (recur (conj acc new-alias) (assoc alias->use-count alias new-count, new-alias 1) more))))))
+
+(defn- select-subclauses->aliases
+  "Return a vector of aliases used in HoneySQL SELECT-SUBCLAUSES.
+   (For clauses that aren't aliased, `nil` is returned as a placeholder)."
+  [select-subclauses]
+  (for [subclause select-subclauses]
+    (when (and (vector? subclause)
+               (= 2 (count subclause)))
+      (second subclause))))
+
+(defn update-select-subclause-aliases
+  "Given a vector of HoneySQL SELECT-SUBCLAUSES and a vector of equal length of NEW-ALIASES,
+   return a new vector with combining the original `SELECT` subclauses with the new aliases.
+
+   Subclauses that are not aliased are not modified; they are given a placeholder of `nil` in the NEW-ALIASES vector.
+
+     (update-select-subclause-aliases [[:user_id \"user_id\"] :venue_id]
+                                      [\"user_id_2\" nil])
+     ;; -> [[:user_id \"user_id_2\"] :venue_id]"
+  [select-subclauses new-aliases]
+  (for [[subclause new-alias] (partition 2 (interleave select-subclauses new-aliases))]
+    (if-not new-alias
+      subclause
+      [(first subclause) new-alias])))
+
+(defn- deduplicate-select-aliases
+  "Replace duplicate aliases in SELECT-SUBCLAUSES with appropriately suffixed aliases.
+
+   BigQuery doesn't allow duplicate aliases in `SELECT` statements; a statement like `SELECT sum(x) AS sum, sum(y) AS sum` is invalid. (See #4089)
+   To work around this, we'll modify the HoneySQL aliases to make sure the same one isn't used twice by suffixing duplicates appropriately.
+   (We'll generate SQL like `SELECT sum(x) AS sum, sum(y) AS sum_2` instead.)"
+  [select-subclauses]
+  (let [aliases (select-subclauses->aliases select-subclauses)
+        deduped (deduplicate-aliases aliases)]
+    (update-select-subclause-aliases select-subclauses deduped)))
+
+(defn- apply-aggregation
+  "BigQuery's implementation of `apply-aggregation` just hands off to the normal Generic SQL implementation, but calls `deduplicate-select-aliases` on the results."
+  [driver honeysql-form query]
+  (-> (sqlqp/apply-aggregation driver honeysql-form query)
+      (update :select deduplicate-select-aliases)))
+
+
+(defn- field->breakout-identifier [field]
   (hsql/raw (str \[ (field->alias field) \])))
 
 (defn- apply-breakout [honeysql-form {breakout-fields :breakout, fields-fields :fields}]
   (-> honeysql-form
       ;; Group by all the breakout fields
-      ((partial apply h/group)  (map field->identitfier breakout-fields))
+      ((partial apply h/group)  (map field->breakout-identifier breakout-fields))
       ;; Add fields form only for fields that weren't specified in :fields clause -- we don't want to include it twice, or HoneySQL will barf
       ((partial apply h/merge-select) (for [field breakout-fields
                                             :when (not (contains? (set fields-fields) field))]
@@ -372,9 +431,9 @@
 
 (defn- apply-order-by [honeysql-form {subclauses :order-by}]
   (loop [honeysql-form honeysql-form, [{:keys [field direction]} & more] subclauses]
-    (let [honeysql-form (h/merge-order-by honeysql-form [(field->identitfier field) (case direction
-                                                                                      :ascending  :asc
-                                                                                      :descending :desc)])]
+    (let [honeysql-form (h/merge-order-by honeysql-form [(field->breakout-identifier field) (case direction
+                                                                                              :ascending  :asc
+                                                                                              :descending :desc)])]
       (if (seq more)
         (recur honeysql-form more)
         honeysql-form))))
@@ -382,6 +441,12 @@
 (defn- string-length-fn [field-key]
   (hsql/call :length field-key))
 
+;; From the dox: Fields must contain only letters, numbers, and underscores, start with a letter or underscore, and be at most 128 characters long.
+(defn- format-custom-field-name ^String [^String custom-field-name]
+  (s/join (take 128 (-> (s/trim custom-field-name)
+                        (s/replace #"[^\w\d_]" "_")
+                        (s/replace #"(^\d)" "_$1")))))
+
 
 (defrecord BigQueryDriver []
   clojure.lang.Named
@@ -392,7 +457,8 @@
 (u/strict-extend BigQueryDriver
   sql/ISQLDriver
   (merge (sql/ISQLDriverDefaultsMixin)
-         {:apply-breakout            (u/drop-first-arg apply-breakout)
+         {:apply-aggregation         apply-aggregation
+          :apply-breakout            (u/drop-first-arg apply-breakout)
           :apply-order-by            (u/drop-first-arg apply-order-by)
           :column->base-type         (constantly nil)                           ; these two are actually not applicable
           :connection-details->spec  (constantly nil)                           ; since we don't use JDBC
@@ -407,46 +473,45 @@
 
   driver/IDriver
   (merge driver/IDriverDefaultsMixin
-         {:analyze-table         analyze/generic-analyze-table
-          :can-connect?          (u/drop-first-arg can-connect?)
-          :date-interval         (u/drop-first-arg (comp prepare-value u/relative-date))
-          :describe-database     (u/drop-first-arg describe-database)
-          :describe-table        (u/drop-first-arg describe-table)
-          :details-fields        (constantly [{:name         "project-id"
-                                               :display-name "Project ID"
-                                               :placeholder  "praxis-beacon-120871"
-                                               :required     true}
-                                              {:name         "dataset-id"
-                                               :display-name "Dataset ID"
-                                               :placeholder  "toucanSightings"
-                                               :required     true}
-                                              {:name         "client-id"
-                                               :display-name "Client ID"
-                                               :placeholder  "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com"
-                                               :required     true}
-                                              {:name         "client-secret"
-                                               :display-name "Client Secret"
-                                               :placeholder  "dJNi4utWgMzyIFo2JbnsK6Np"
-                                               :required     true}
-                                              {:name         "auth-code"
-                                               :display-name "Auth Code"
-                                               :placeholder  "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek"
-                                               :required     true}])
-          :execute-query         (u/drop-first-arg execute-query)
+         {:analyze-table            analyze/generic-analyze-table
+          :can-connect?             (u/drop-first-arg can-connect?)
+          :date-interval            (u/drop-first-arg (comp prepare-value u/relative-date))
+          :describe-database        (u/drop-first-arg describe-database)
+          :describe-table           (u/drop-first-arg describe-table)
+          :details-fields           (constantly [{:name         "project-id"
+                                                  :display-name "Project ID"
+                                                  :placeholder  "praxis-beacon-120871"
+                                                  :required     true}
+                                                 {:name         "dataset-id"
+                                                  :display-name "Dataset ID"
+                                                  :placeholder  "toucanSightings"
+                                                  :required     true}
+                                                 {:name         "client-id"
+                                                  :display-name "Client ID"
+                                                  :placeholder  "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com"
+                                                  :required     true}
+                                                 {:name         "client-secret"
+                                                  :display-name "Client Secret"
+                                                  :placeholder  "dJNi4utWgMzyIFo2JbnsK6Np"
+                                                  :required     true}
+                                                 {:name         "auth-code"
+                                                  :display-name "Auth Code"
+                                                  :placeholder  "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek"
+                                                  :required     true}])
+          :execute-query            (u/drop-first-arg execute-query)
           ;; Don't enable foreign keys when testing because BigQuery *doesn't* have a notion of foreign keys. Joins are still allowed, which puts us in a weird position, however;
           ;; people can manually specifiy "foreign key" relationships in admin and everything should work correctly.
           ;; Since we can't infer any "FK" relationships during sync our normal FK tests are not appropriate for BigQuery, so they're disabled for the time being.
           ;; TODO - either write BigQuery-speciifc tests for FK functionality or add additional code to manually set up these FK relationships for FK tables
-          :features              (constantly (set/union #{:basic-aggregations
-                                                          :standard-deviation-aggregations
-                                                          :native-parameters
-                                                          ;; Expression aggregations *would* work, but BigQuery doesn't support the auto-generated column names. BQ column names
-                                                          ;; can only be alphanumeric or underscores. If we slugified the auto-generated column names, we could enable this feature.
-                                                          #_:expression-aggregations}
-                                                        (when-not config/is-test?
-                                                          ;; during unit tests don't treat bigquery as having FK support
-                                                          #{:foreign-keys})))
-          :field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq)
-          :mbql->native          (u/drop-first-arg mbql->native)}))
+          :features                 (constantly (set/union #{:basic-aggregations
+                                                             :standard-deviation-aggregations
+                                                             :native-parameters
+                                                             :expression-aggregations}
+                                                           (when-not config/is-test?
+                                                             ;; during unit tests don't treat bigquery as having FK support
+                                                             #{:foreign-keys})))
+          :field-values-lazy-seq    (u/drop-first-arg field-values-lazy-seq)
+          :format-custom-field-name (u/drop-first-arg format-custom-field-name)
+          :mbql->native             (u/drop-first-arg mbql->native)}))
 
 (driver/register-driver! :bigquery driver)
diff --git a/src/metabase/driver/crate.clj b/src/metabase/driver/crate.clj
index 6d88591a90c6febca681fce0017ddc09e696d769..55082daf911962089bfc19d9a3e481c7ff75aa36 100644
--- a/src/metabase/driver/crate.clj
+++ b/src/metabase/driver/crate.clj
@@ -1,10 +1,12 @@
 (ns metabase.driver.crate
   (:require [clojure.java.jdbc :as jdbc]
+            [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
             [metabase.driver :as driver]
             [metabase.driver.crate.util :as crate-util]
             [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u]))
+            [metabase.util :as u])
+  (:import java.sql.DatabaseMetaData))
 
 (def ^:private ^:const column->base-type
   "Map of Crate column types -> Field base types
@@ -55,6 +57,39 @@
 (defn- string-length-fn [field-key]
   (hsql/call :char_length field-key))
 
+(defn- describe-table-fields
+  [database, driver, {:keys [schema name]}]
+  (let [columns (jdbc/query
+                 (sql/db->jdbc-connection-spec database)
+                 [(format "select column_name, data_type as type_name
+                            from information_schema.columns
+                            where table_name like '%s' and table_schema like '%s'
+                            and data_type != 'object_array'" name schema)])] ; clojure jdbc can't handle fields of type "object_array" atm
+    (set (for [{:keys [column_name type_name]} columns]
+           {:name      column_name
+            :custom    {:column-type type_name}
+            :base-type (or (column->base-type (keyword type_name))
+                           (do (log/warn (format "Don't know how to map column type '%s' to a Field base_type, falling back to :type/*." type_name))
+                               :type/*))}))))
+
+(defn- add-table-pks
+  [^DatabaseMetaData metadata, table]
+  (let [pks (->> (.getPrimaryKeys metadata nil nil (:name table))
+                 jdbc/result-set-seq
+                 (mapv :column_name)
+                 set)]
+    (update table :fields (fn [fields]
+                            (set (for [field fields]
+                                   (if-not (contains? pks (:name field))
+                                     field
+                                     (assoc field :pk? true))))))))
+
+(defn- describe-table [driver database table]
+  (sql/with-metadata [metadata driver database]
+    (->> (describe-table-fields database driver table)
+         (assoc (select-keys table [:name :schema]) :fields)
+         ;; find PKs and mark them
+         (add-table-pks metadata))))
 
 (defrecord CrateDriver []
   clojure.lang.Named
@@ -65,6 +100,7 @@
   (merge (sql/IDriverSQLDefaultsMixin)
          {:can-connect?   (u/drop-first-arg can-connect?)
           :date-interval  crate-util/date-interval
+          :describe-table describe-table
           :details-fields (constantly [{:name         "hosts"
                                         :display-name "Hosts"
                                         :default      "localhost:5432"}])
@@ -75,6 +111,8 @@
           :column->base-type         (u/drop-first-arg column->base-type)
           :string-length-fn          (u/drop-first-arg string-length-fn)
           :date                      crate-util/date
+          :quote-style               (constantly :crate)
+          :field->alias              (constantly nil)
           :unix-timestamp->timestamp crate-util/unix-timestamp->timestamp
           :current-datetime-fn       (constantly now)}))
 
diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj
index 7ec2887b4fc640deb8b0540d77b50901a5bac52c..8befd598664fa16eb7084a10eafe76a478045cd7 100644
--- a/src/metabase/driver/druid/query_processor.clj
+++ b/src/metabase/driver/druid/query_processor.clj
@@ -577,15 +577,16 @@
                                                      {:dimension (->rvalue field)
                                                       :direction direction}))))
 
-(defmethod handle-order-by ::grouped-timeseries [_ {[breakout-field] :breakout, [{field :field, direction :direction}] :order-by} druid-query]
-  (let [field             (->rvalue field)
-        breakout-field    (->rvalue breakout-field)
-        sort-by-breakout? (= field breakout-field)]
-    (if (and sort-by-breakout?
-             (= direction :descending))
-      (assoc druid-query :descending true)
-      druid-query)))
+;; Handle order by timstamp field
+(defn- handle-order-by-timestamp [field direction druid-query]
+  (assoc druid-query :descending (and (instance? DateTimeField field)
+                                      (= direction :descending))))
+
+(defmethod handle-order-by ::grouped-timeseries [_ {[{field :field, direction :direction}] :order-by} druid-query]
+  (handle-order-by-timestamp field direction druid-query))
 
+(defmethod handle-order-by ::select [_ {[{field :field, direction :direction}] :order-by} druid-query]
+  (handle-order-by-timestamp field direction druid-query))
 
 ;;; ### handle-fields
 
diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index 4c1364d7ba01a6182f3faa329a72342280d94834..dbec2699e8e5f9c135fab69c7843b3bfcb5edf3b 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -27,9 +27,12 @@
    Methods marked *OPTIONAL* have default implementations in `ISQLDriverDefaultsMixin`."
 
   (active-tables ^java.util.Set [this, ^DatabaseMetaData metadata]
-    "Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE.
+    "*OPTIONAL* Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE.
      Each map should contain the key `:name`, which is the string name of the table. For databases that have a concept of schemas,
-     this map should also include the string name of the table's `:schema`.")
+     this map should also include the string name of the table's `:schema`.
+
+   Two different implementations are provided in this namespace: `fast-active-tables` (the default), and `post-filtered-active-tables`. You should be fine using
+   the default, but refer to the documentation for those functions for more details on the differences.")
 
   ;; The following apply-* methods define how the SQL Query Processor handles given query clauses. Each method is called when a matching clause is present
   ;; in QUERY, and should return an appropriately modified version of KORMA-QUERY. Most drivers can use the default implementations for all of these methods,
@@ -71,7 +74,7 @@
   (field-percent-urls [this field]
     "*OPTIONAL*. Implementation of the `:field-percent-urls-fn` to be passed to `make-analyze-table`.
      The default implementation is `fast-field-percent-urls`, which avoids a full table scan. Substitue this with `slow-field-percent-urls` for databases
-     where this doesn't work, such as SQL Server")
+     where this doesn't work, such as SQL Server.")
 
   (field->alias ^String [this, ^Field field]
     "*OPTIONAL*. Return the alias that should be used to for FIELD, i.e. in an `AS` clause. The default implementation calls `name`, which
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index 3a19281b242c7d0705e087f545cd94d2a132088c..741a791a1a35a4ea6836755639fe631c7b6bd9da 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -151,7 +151,7 @@
 
 (defn- apply-expression-aggregation [driver honeysql-form expression]
   (h/merge-select honeysql-form [(expression-aggregation->honeysql driver expression)
-                                 (hx/escape-dots (annotate/aggregation-name expression))]))
+                                 (hx/escape-dots (driver/format-custom-field-name driver (annotate/aggregation-name expression)))]))
 
 (defn- apply-single-aggregation [driver honeysql-form {:keys [aggregation-type field], :as aggregation}]
   (h/merge-select honeysql-form [(aggregation->honeysql driver aggregation-type field)
diff --git a/src/metabase/driver/generic_sql/util/unprepare.clj b/src/metabase/driver/generic_sql/util/unprepare.clj
index d65502ac180912785228c1ac8058e590ed548f71..56845e692a68a5e0a3500406c0c5609ebed4ffcd 100644
--- a/src/metabase/driver/generic_sql/util/unprepare.clj
+++ b/src/metabase/driver/generic_sql/util/unprepare.clj
@@ -7,20 +7,20 @@
   (:import java.util.Date))
 
 (defprotocol ^:private IUnprepare
-  (^:private unprepare-arg ^String [this]))
+  (^:private unprepare-arg ^String [this settings]))
 
 (extend-protocol IUnprepare
-  nil     (unprepare-arg [this] "NULL")
-  String  (unprepare-arg [this] (str \' (str/replace this "'" "\\\\'") \')) ; escape single-quotes
-  Boolean (unprepare-arg [this] (if this "TRUE" "FALSE"))
-  Number  (unprepare-arg [this] (str this))
-  Date    (unprepare-arg [this] (first (hsql/format (hsql/call :timestamp (hx/literal (u/date->iso-8601 this))))))) ; TODO - this probably doesn't work for every DB!
+  nil     (unprepare-arg [this _] "NULL")
+  String  (unprepare-arg [this {:keys [quote-escape]}] (str \' (str/replace this "'" (str quote-escape "'")) \')) ; escape single-quotes
+  Boolean (unprepare-arg [this _] (if this "TRUE" "FALSE"))
+  Number  (unprepare-arg [this _] (str this))
+  Date    (unprepare-arg [this {:keys [iso-8601-fn]}] (first (hsql/format (hsql/call iso-8601-fn (hx/literal (u/date->iso-8601 this)))))))
 
 (defn unprepare
   "Convert a normal SQL `[statement & prepared-statement-args]` vector into a flat, non-prepared statement."
-  ^String [[sql & args]]
+  ^String [[sql & args] & {:keys [quote-escape iso-8601-fn], :or {quote-escape "\\\\", iso-8601-fn :timestamp}}]
   (loop [sql sql, [arg & more-args, :as args] args]
     (if-not (seq args)
       sql
-      (recur (str/replace-first sql #"(?<!\?)\?(?!\?)" (unprepare-arg arg))
+      (recur (str/replace-first sql #"(?<!\?)\?(?!\?)" (unprepare-arg arg {:quote-escape quote-escape, :iso-8601-fn iso-8601-fn}))
              more-args))))
diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj
index 4e27d66d57140600f9380fc92e11de81b4643a67..4dac3f89814c8677bfd63ebbcaad925229b68545 100644
--- a/src/metabase/driver/mongo/query_processor.clj
+++ b/src/metabase/driver/mongo/query_processor.clj
@@ -379,31 +379,63 @@
                     v)}))))
 
 
-;;; ------------------------------------------------------------ Handling ISODate(...) forms ------------------------------------------------------------
-;; In Mongo it's fairly common use ISODate(...) forms in queries, which unfortunately are not valid JSON,
+;;; ------------------------------------------------------------ Handling ISODate(...) and ObjectId(...) forms ------------------------------------------------------------
+;; In Mongo it's fairly common use ISODate(...) or ObjectId(...) forms in queries, which unfortunately are not valid JSON,
 ;; and thus cannot be parsed by Cheshire. But we are clever so we will:
 ;;
 ;; 1) Convert forms like ISODate(...) to valid JSON forms like ["___ISODate", ...]
 ;; 2) Parse Normally
-;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates
+;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates, and [:___ObjectId ...] to BSON IDs
+
+;; add more fn handlers here as needed
+(def ^:private fn-name->decoder
+  {:ISODate (fn [arg]
+              (DateTime. arg))
+   :ObjectId (fn [^String arg]
+               (ObjectId. arg))})
+
+(defn- form->encoded-fn-name
+  "If FORM is an encoded fn call form return the key representing the fn call that was encoded.
+   If it doesn't represent an encoded fn, return `nil`.
+
+     (form->encoded-fn-name [:___ObjectId \"583327789137b2700a1621fb\"]) -> :ObjectId"
+  [form]
+  (when (vector? form)
+    (when (u/string-or-keyword? (first form))
+      (when-let [[_ k] (re-matches #"^___(\w+$)" (name (first form)))]
+        (let [k (keyword k)]
+          (when (contains? fn-name->decoder k)
+            k))))))
+
+(defn- maybe-decode-fncall [form]
+  (if-let [fn-name (form->encoded-fn-name form)]
+    ((fn-name->decoder fn-name) (second form))
+    form))
 
-(defn- encoded-iso-date? [form]
-  (and (vector? form)
-       (= (first form) "___ISODate")))
+(defn- decode-fncalls [query]
+  (walk/postwalk maybe-decode-fncall query))
 
-(defn- maybe-decode-iso-date-fncall [form]
-  (if (encoded-iso-date? form)
-    (DateTime. (second form))
-    form))
+(defn- encode-fncalls-for-fn
+  "Walk QUERY-STRING and replace fncalls to fn with FN-NAME with encoded forms that can be parsed as valid JSON.
+
+     (encode-fncalls-for-fn \"ObjectId\" \"{\\\"$match\\\":ObjectId(\\\"583327789137b2700a1621fb\\\")}\")
+     ;; -> \"{\\\"$match\\\":[\\\"___ObjectId\\\", \\\"583327789137b2700a1621fb\\\"]}\""
+  [fn-name query-string]
+  (s/replace query-string
+             (re-pattern (format "%s\\(([^)]+)\\)" (name fn-name)))
+             (format "[\"___%s\", $1]" (name fn-name))))
 
-(defn- decode-iso-date-fncalls [query]
-  (walk/postwalk maybe-decode-iso-date-fncall query))
+(defn- encode-fncalls
+  "Replace occurances of `ISODate(...)` and similary function calls (invalid JSON, but legal in Mongo)
+   with legal JSON forms like `[:___ISODate ...]` that we can decode later.
 
-(defn- encode-iso-date-fncalls
-  "Replace occurances of `ISODate(...)` function calls (invalid JSON, but legal in Mongo)
-   with legal JSON forms like `[:___ISODate ...]` that we can decode later."
+   Walks QUERY-STRING and encodes all the various fncalls we support."
   [query-string]
-  (s/replace query-string #"ISODate\(([^)]+)\)" "[\"___ISODate\", $1]"))
+  (loop [query-string query-string, [fn-name & more] (keys fn-name->decoder)]
+    (if-not fn-name
+      query-string
+      (recur (encode-fncalls-for-fn fn-name query-string)
+             more))))
 
 
 ;;; ------------------------------------------------------------ Query Execution ------------------------------------------------------------
@@ -427,7 +459,7 @@
          (string? collection)
          (map? database)]}
   (let [query   (if (string? query)
-                  (decode-iso-date-fncalls (json/parse-string (encode-iso-date-fncalls query) keyword))
+                  (decode-fncalls (json/parse-string (encode-fncalls query) keyword))
                   query)
         results (mc/aggregate *mongo-connection* collection query
                               :allow-disk-use true)
diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj
new file mode 100644
index 0000000000000000000000000000000000000000..97630c44ff7b08284a7b8d5a11700d335207def3
--- /dev/null
+++ b/src/metabase/driver/presto.clj
@@ -0,0 +1,344 @@
+(ns metabase.driver.presto
+  (:require [clojure.set :as set]
+            [clojure.string :as str]
+            [clj-http.client :as http]
+            (honeysql [core :as hsql]
+                      [helpers :as h])
+            [metabase.config :as config]
+            [metabase.driver :as driver]
+            [metabase.driver.generic-sql :as sql]
+            [metabase.driver.generic-sql.util.unprepare :as unprepare]
+            (metabase.models [field :as field]
+                             [table :as table])
+            [metabase.sync-database.analyze :as analyze]
+            [metabase.query-processor.util :as qputil]
+            [metabase.util :as u]
+            [metabase.util.honeysql-extensions :as hx])
+  (:import java.util.Date
+           (metabase.query_processor.interface DateTimeValue Value)))
+
+
+;;; Presto API helpers
+
+(defn- details->uri
+  [{:keys [ssl host port]} path]
+  (str (if ssl "https" "http") "://" host ":" port path))
+
+(defn- details->request [{:keys [user password catalog report-timezone]}]
+  (merge {:headers (merge {"X-Presto-Source" "metabase"
+                           "X-Presto-User"   user}
+                          (when catalog
+                            {"X-Presto-Catalog" catalog})
+                          (when report-timezone
+                            {"X-Presto-Time-Zone" report-timezone}))}
+         (when password
+           {:basic-auth [user password]})))
+
+(defn- parse-time-with-tz [s]
+  ;; Try parsing with offset first then with full ZoneId
+  (or (u/ignore-exceptions (u/parse-date "HH:mm:ss.SSS ZZ" s))
+      (u/parse-date "HH:mm:ss.SSS ZZZ" s)))
+
+(defn- parse-timestamp-with-tz [s]
+  ;; Try parsing with offset first then with full ZoneId
+  (or (u/ignore-exceptions (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s))
+      (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s)))
+
+(defn- field-type->parser [field-type]
+  (condp re-matches field-type
+    #"decimal.*"                bigdec
+    #"time"                     (partial u/parse-date :hour-minute-second-ms)
+    #"time with time zone"      parse-time-with-tz
+    #"timestamp"                (partial u/parse-date "yyyy-MM-dd HH:mm:ss.SSS")
+    #"timestamp with time zone" parse-timestamp-with-tz
+    #".*"                       identity))
+
+(defn- parse-presto-results [columns data]
+  (let [parsers (map (comp field-type->parser :type) columns)]
+    (for [row data]
+      (for [[value parser] (partition 2 (interleave row parsers))]
+        (when value
+          (parser value))))))
+
+(defn- fetch-presto-results! [details {prev-columns :columns, prev-rows :rows} uri]
+  (let [{{:keys [columns data nextUri error]} :body} (http/get uri (assoc (details->request details) :as :json))]
+    (when error
+      (throw (ex-info (or (:message error) "Error running query.") error)))
+    (let [rows    (parse-presto-results columns data)
+          results {:columns (or columns prev-columns)
+                   :rows    (vec (concat prev-rows rows))}]
+      (if (nil? nextUri)
+        results
+        (do (Thread/sleep 100) ; Might not be the best way, but the pattern is that we poll Presto at intervals
+            (fetch-presto-results! details results nextUri))))))
+
+(defn- execute-presto-query! [details query]
+  (let [{{:keys [columns data nextUri error]} :body} (http/post (details->uri details "/v1/statement")
+                                                                (assoc (details->request details) :body query, :as :json))]
+    (when error
+      (throw (ex-info (or (:message error) "Error preparing query.") error)))
+    (let [rows    (parse-presto-results (or columns []) (or data []))
+          results {:columns (or columns [])
+                   :rows    rows}]
+      (if (nil? nextUri)
+        results
+        (fetch-presto-results! details results nextUri)))))
+
+
+;;; Generic helpers
+
+(defn- quote-name [nm]
+  (str \" (str/replace nm "\"" "\"\"") \"))
+
+(defn- quote+combine-names [& names]
+  (str/join \. (map quote-name names)))
+
+
+;;; IDriver implementation
+
+(defn- field-avg-length [{field-name :name, :as field}]
+  (let [table             (field/table field)
+        {:keys [details]} (table/database table)
+        sql               (format "SELECT cast(round(avg(length(%s))) AS integer) FROM %s WHERE %s IS NOT NULL"
+                            (quote-name field-name)
+                            (quote+combine-names (:schema table) (:name table))
+                            (quote-name field-name))
+        {[[v]] :rows}     (execute-presto-query! details sql)]
+    (or v 0)))
+
+(defn- field-percent-urls [{field-name :name, :as field}]
+  (let [table             (field/table field)
+        {:keys [details]} (table/database table)
+        sql               (format "SELECT cast(count_if(url_extract_host(%s) <> '') AS double) / cast(count(*) AS double) FROM %s WHERE %s IS NOT NULL"
+                            (quote-name field-name)
+                            (quote+combine-names (:schema table) (:name table))
+                            (quote-name field-name))
+        {[[v]] :rows}     (execute-presto-query! details sql)]
+    (if (= v "NaN") 0.0 v)))
+
+(defn- analyze-table [driver table new-table-ids]
+  ((analyze/make-analyze-table driver
+     :field-avg-length-fn   field-avg-length
+     :field-percent-urls-fn field-percent-urls) driver table new-table-ids))
+
+(defn- can-connect? [{:keys [catalog] :as details}]
+  (let [{[[v]] :rows} (execute-presto-query! details (str "SHOW SCHEMAS FROM " (quote-name catalog) " LIKE 'information_schema'"))]
+    (= v "information_schema")))
+
+(defn- date-interval [unit amount]
+  (hsql/call :date_add (hx/literal unit) amount :%now))
+
+(defn- describe-schema [{{:keys [catalog] :as details} :details} {:keys [schema]}]
+  (let [sql            (str "SHOW TABLES FROM " (quote+combine-names catalog schema))
+        {:keys [rows]} (execute-presto-query! details sql)
+        tables         (map first rows)]
+    (set (for [name tables]
+           {:name name, :schema schema}))))
+
+(defn- describe-database [{{:keys [catalog] :as details} :details :as database}]
+  (let [sql            (str "SHOW SCHEMAS FROM " (quote-name catalog))
+        {:keys [rows]} (execute-presto-query! details sql)
+        schemas        (remove #{"information_schema"} (map first rows))] ; inspecting "information_schema" breaks weirdly
+    {:tables (apply set/union (for [name schemas]
+                                (describe-schema database {:schema name})))}))
+
+(defn- presto-type->base-type [field-type]
+  (condp re-matches field-type
+    #"boolean"     :type/Boolean
+    #"tinyint"     :type/Integer
+    #"smallint"    :type/Integer
+    #"integer"     :type/Integer
+    #"bigint"      :type/BigInteger
+    #"real"        :type/Float
+    #"double"      :type/Float
+    #"decimal.*"   :type/Decimal
+    #"varchar.*"   :type/Text
+    #"char.*"      :type/Text
+    #"varbinary.*" :type/*
+    #"json"        :type/Text       ; TODO - this should probably be Dictionary or something
+    #"date"        :type/Date
+    #"time.*"      :type/DateTime
+    #"array"       :type/Array
+    #"map"         :type/Dictionary
+    #"row.*"       :type/*          ; TODO - again, but this time we supposedly have a schema
+    #".*"          :type/*))
+
+(defn- describe-table [{{:keys [catalog] :as details} :details} {schema :schema, table-name :name}]
+  (let [sql            (str "DESCRIBE " (quote+combine-names catalog schema table-name))
+        {:keys [rows]} (execute-presto-query! details sql)]
+    {:schema schema
+     :name   table-name
+     :fields (set (for [[name type] rows]
+                    {:name name, :base-type (presto-type->base-type type)}))}))
+
+(defprotocol ^:private IPrepareValue
+  (^:private prepare-value [this]))
+(extend-protocol IPrepareValue
+  nil           (prepare-value [_] nil)
+  DateTimeValue (prepare-value [{:keys [value]}] (prepare-value value))
+  Value         (prepare-value [{:keys [value]}] (prepare-value value))
+  String        (prepare-value [this] (hx/literal (str/replace this "'" "''")))
+  Boolean       (prepare-value [this] (hsql/raw (if this "TRUE" "FALSE")))
+  Date          (prepare-value [this] (hsql/call :from_iso8601_timestamp (hx/literal (u/date->iso-8601 this))))
+  Number        (prepare-value [this] this)
+  Object        (prepare-value [this] (throw (Exception. (format "Don't know how to prepare value %s %s" (class this) this)))))
+
+(defn- execute-query [{:keys [database settings], {sql :query, params :params} :native, :as outer-query}]
+  (let [sql                    (str "-- " (qputil/query->remark outer-query) "\n"
+                                          (unprepare/unprepare (cons sql params) :quote-escape "'", :iso-8601-fn  :from_iso8601_timestamp))
+        details                (merge (:details database) settings)
+        {:keys [columns rows]} (execute-presto-query! details sql)]
+    {:columns (map (comp keyword :name) columns)
+     :rows    rows}))
+
+(defn- field-values-lazy-seq [{field-name :name, :as field}]
+  ;; TODO - look into making this actually lazy
+  (let [table             (field/table field)
+        {:keys [details]} (table/database table)
+        sql               (format "SELECT %s FROM %s LIMIT %d"
+                            (quote-name field-name)
+                            (quote+combine-names (:schema table) (:name table))
+                            driver/max-sync-lazy-seq-results)
+        {:keys [rows]}    (execute-presto-query! details sql)]
+    (for [row rows]
+      (first row))))
+
+(defn- humanize-connection-error-message [message]
+  (condp re-matches message
+    #"^java.net.ConnectException: Connection refused.*$"
+    (driver/connection-error-messages :cannot-connect-check-host-and-port)
+
+    #"^clojure.lang.ExceptionInfo: Catalog .* does not exist.*$"
+    (driver/connection-error-messages :database-name-incorrect)
+
+    #"^java.net.UnknownHostException.*$"
+    (driver/connection-error-messages :invalid-hostname)
+
+    #".*" ; default
+    message))
+
+(defn- table-rows-seq [{:keys [details]} {:keys [schema name]}]
+  (let [sql                        (format "SELECT * FROM %s" (quote+combine-names schema name))
+        {:keys [rows], :as result} (execute-presto-query! details sql)
+        columns                    (map (comp keyword :name) (:columns result))]
+    (for [row rows]
+      (zipmap columns row))))
+
+
+;;; ISQLDriver implementation
+
+(defn- apply-page [honeysql-query {{:keys [items page]} :page}]
+  (let [offset (* (dec page) items)]
+    (if (zero? offset)
+      ;; if there's no offset we can simply use limit
+      (h/limit honeysql-query items)
+      ;; if we need to do an offset we have to do nesting to generate a row number and where on that
+      (let [over-clause (format "row_number() OVER (%s)"
+                                (first (hsql/format (select-keys honeysql-query [:order-by])
+                                                    :allow-dashed-names? true
+                                                    :quoting :ansi)))]
+        (-> (apply h/select (map last (:select honeysql-query)))
+            (h/from (h/merge-select honeysql-query [(hsql/raw over-clause) :__rownum__]))
+            (h/where [:> :__rownum__ offset])
+            (h/limit items))))))
+
+(defn- date [unit expr]
+  (case unit
+    :default         expr
+    :minute          (hsql/call :date_trunc (hx/literal :minute) expr)
+    :minute-of-hour  (hsql/call :minute expr)
+    :hour            (hsql/call :date_trunc (hx/literal :hour) expr)
+    :hour-of-day     (hsql/call :hour expr)
+    :day             (hsql/call :date_trunc (hx/literal :day) expr)
+    ;; Presto is ISO compliant, so we need to offset Monday = 1 to Sunday = 1
+    :day-of-week     (hx/+ (hx/mod (hsql/call :day_of_week expr) 7) 1)
+    :day-of-month    (hsql/call :day expr)
+    :day-of-year     (hsql/call :day_of_year expr)
+    ;; Similar to DoW, sicne Presto is ISO compliant the week starts on Monday, we need to shift that to Sunday
+    :week            (hsql/call :date_add (hx/literal :day) -1 (hsql/call :date_trunc (hx/literal :week) (hsql/call :date_add (hx/literal :day) 1 expr)))
+    ;; Offset by one day forward to "fake" a Sunday starting week
+    :week-of-year    (hsql/call :week (hsql/call :date_add (hx/literal :day) 1 expr))
+    :month           (hsql/call :date_trunc (hx/literal :month) expr)
+    :month-of-year   (hsql/call :month expr)
+    :quarter         (hsql/call :date_trunc (hx/literal :quarter) expr)
+    :quarter-of-year (hsql/call :quarter expr)
+    :year            (hsql/call :year expr)))
+
+(defn- string-length-fn [field-key]
+  (hsql/call :length field-key))
+
+(defn- unix-timestamp->timestamp [expr seconds-or-milliseconds]
+  (case seconds-or-milliseconds
+    :seconds      (hsql/call :from_unixtime expr)
+    :milliseconds (recur (hx// expr 1000.0) :seconds)))
+
+
+;;; Driver implementation
+
+(defrecord PrestoDriver []
+  clojure.lang.Named
+  (getName [_] "Presto"))
+
+(u/strict-extend PrestoDriver
+  driver/IDriver
+  (merge (sql/IDriverSQLDefaultsMixin)
+         {:analyze-table                     analyze-table
+          :can-connect?                      (u/drop-first-arg can-connect?)
+          :date-interval                     (u/drop-first-arg date-interval)
+          :describe-database                 (u/drop-first-arg describe-database)
+          :describe-table                    (u/drop-first-arg describe-table)
+          :describe-table-fks                (constantly nil) ; no FKs in Presto
+          :details-fields                    (constantly [{:name         "host"
+                                                           :display-name "Host"
+                                                           :default      "localhost"}
+                                                          {:name         "port"
+                                                           :display-name "Port"
+                                                           :type         :integer
+                                                           :default      8080}
+                                                          {:name         "catalog"
+                                                           :display-name "Database name"
+                                                           :placeholder  "hive"
+                                                           :required     true}
+                                                          {:name         "user"
+                                                           :display-name "Database username"
+                                                           :placeholder  "What username do you use to login to the database"
+                                                           :default      "metabase"}
+                                                          {:name         "password"
+                                                           :display-name "Database password"
+                                                           :type         :password
+                                                           :placeholder  "*******"}
+                                                          {:name         "ssl"
+                                                           :display-name "Use a secure connection (SSL)?"
+                                                           :type         :boolean
+                                                           :default      false}])
+          :execute-query                     (u/drop-first-arg execute-query)
+          :features                          (constantly (set/union #{:set-timezone
+                                                                      :basic-aggregations
+                                                                      :standard-deviation-aggregations
+                                                                      :expressions
+                                                                      :native-parameters
+                                                                      :expression-aggregations}
+                                                                    (when-not config/is-test?
+                                                                      ;; during unit tests don't treat presto as having FK support
+                                                                      #{:foreign-keys})))
+          :field-values-lazy-seq             (u/drop-first-arg field-values-lazy-seq)
+          :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)
+          :table-rows-seq                    (u/drop-first-arg table-rows-seq)})
+
+  sql/ISQLDriver
+  (merge (sql/ISQLDriverDefaultsMixin)
+         {:apply-page                (u/drop-first-arg apply-page)
+          :column->base-type         (constantly nil)
+          :connection-details->spec  (constantly nil)
+          :current-datetime-fn       (constantly :%now)
+          :date                      (u/drop-first-arg date)
+          :excluded-schemas          (constantly #{"information_schema"})
+          :field-percent-urls        (u/drop-first-arg field-percent-urls)
+          :prepare-value             (u/drop-first-arg prepare-value)
+          :quote-style               (constantly :ansi)
+          :stddev-fn                 (constantly :stddev_samp)
+          :string-length-fn          (u/drop-first-arg string-length-fn)
+          :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
+
+
+(driver/register-driver! :presto (PrestoDriver.))
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 0bbc09ffc3fb8b3a32d263a82ad25fdbf0ec3499..9731b50acdbdae802f760dda254ccf090546ef74 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -6,7 +6,8 @@
             [metabase.util :as u])
   (:import javax.mail.Session))
 
-;; ## CONFIG
+;;; CONFIG
+;; TODO - smtp-port should be switched to type :integer
 
 (defsetting email-from-address  "Email address you want to use as the sender of Metabase." :default "notifications@metabase.com")
 (defsetting email-smtp-host     "The address of the SMTP server that handles your emails.")
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index f460dac615cd142fa07cc597e4c9ec6ac8160221..88f19c572b8683105bff19f4bfbbbbf5b27c4800 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -92,10 +92,10 @@
   [session-id]
   (when (and session-id (init-status/complete?))
     (when-let [session (or (session-with-id session-id)
-                           (println "no matching session with ID") ; NOCOMMIT
+                           (println "no matching session with ID") ; DEBUG
                            )]
       (if (session-expired? session)
-        (println (format "session-is-expired! %d min / %d min" (session-age-minutes session) (config/config-int :max-session-age))) ; NOCOMMIT
+        (printf "session-is-expired! %d min / %d min\n" (session-age-minutes session) (config/config-int :max-session-age)) ; DEBUG
         {:metabase-user-id (:user_id session)
          :is-superuser?    (:is_superuser session)}))))
 
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index 2dedbb90f356e56bd0f8adcb87102d70b07c4b31..660819acfdaf2294b49759750e760996da2c48ec 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -88,23 +88,24 @@
          :creator_id  user-id)
        (events/publish-event! :dashboard-create)))
 
+
+
 (defn update-dashboard!
   "Update a `Dashboard`"
-  [{:keys [id name description parameters caveats points_of_interest show_in_getting_started enable_embedding embedding_params], :as dashboard} user-id]
+  [dashboard user-id]
   {:pre [(map? dashboard)
-         (integer? id)
-         (u/maybe? u/sequence-of-maps? parameters)
+         (u/maybe? u/sequence-of-maps? (:parameters dashboard))
          (integer? user-id)]}
-  (db/update-non-nil-keys! Dashboard id
-    :description             description
-    :name                    name
-    :parameters              parameters
-    :caveats                 caveats
-    :points_of_interest      points_of_interest
-    :enable_embedding        enable_embedding
-    :embedding_params        embedding_params
-    :show_in_getting_started show_in_getting_started)
-  (u/prog1 (Dashboard id)
+  (db/update! Dashboard (u/get-id dashboard)
+    (merge
+     ;; description is allowed to be `nil`
+     (when (contains? dashboard :description)
+       {:description (:description dashboard)})
+     ;; only set everything else if its non-nil
+     (into {} (for [k     [:name :parameters :caveats :points_of_interest :show_in_getting_started :enable_embedding :embedding_params]
+                    :when (k dashboard)]
+                {k (k dashboard)}))))
+  (u/prog1 (Dashboard (u/get-id dashboard))
     (events/publish-event! :dashboard-update (assoc <> :actor_id user-id))))
 
 
diff --git a/src/metabase/models/humanization.clj b/src/metabase/models/humanization.clj
index 0780fd56b7b2c9887100965d6c9cd05b13d62c5b..0878761bf883ac73bea68073a9cd14282847bad7 100644
--- a/src/metabase/models/humanization.clj
+++ b/src/metabase/models/humanization.clj
@@ -83,7 +83,7 @@
   (re-humanize-table-and-field-names!))
 
 (defsetting enable-advanced-humanization
-  "Metabase can attempt to transform your table and field names into more sensible human readable versions, e.g. \"somehorriblename\" becomes \"Some Horrible Name\".
+  "Metabase can attempt to transform your table and field names into more sensible, human-readable versions, e.g. \"somehorriblename\" becomes \"Some Horrible Name\".
    This doesn’t work all that well if the names are in a language other than English, however. Do you want us to take a guess?"
   :type    :boolean
   :default true
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 804877b9dd93f5c4cba623b15d2d48aa53bb7016..308210288029306ae3d095a390eb1a00a84bfffd 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -1,15 +1,20 @@
 (ns metabase.models.interface
   (:require [clojure.core.memoize :as memoize]
             [cheshire.core :as json]
+            [taoensso.nippy :as nippy]
             [toucan.models :as models]
             [metabase.config :as config]
             [metabase.util :as u]
-            [metabase.util.encryption :as encryption]))
+            [metabase.util.encryption :as encryption])
+  (:import java.sql.Blob))
 
 ;;; ------------------------------------------------------------ Toucan Extensions ------------------------------------------------------------
 
 (models/set-root-namespace! 'metabase.models)
 
+
+;;; types
+
 (defn- json-in [obj]
   (if (string? obj)
     obj
@@ -39,6 +44,24 @@
   :in  encrypted-json-in
   :out (comp cached-encrypted-json-out u/jdbc-clob->str))
 
+(defn compress
+  "Compress OBJ, returning a byte array."
+  [obj]
+  (nippy/freeze obj {:compressor nippy/snappy-compressor}))
+
+(defn decompress
+  "Decompress COMPRESSED-BYTES."
+  [compressed-bytes]
+  (if (instance? Blob compressed-bytes)
+    (recur (.getBytes ^Blob compressed-bytes 0 (.length ^Blob compressed-bytes)))
+    (nippy/thaw compressed-bytes {:compressor nippy/snappy-compressor})))
+
+(models/add-type! :compressed
+  :in  compress
+  :out decompress)
+
+
+;;; properties
 
 (defn- add-created-at-timestamp [obj & _]
   (assoc obj :created_at (u/new-sql-timestamp)))
@@ -50,6 +73,11 @@
   :insert (comp add-created-at-timestamp add-updated-at-timestamp)
   :update add-updated-at-timestamp)
 
+;; like `timestamped?`, but for models that only have an `:updated_at` column
+(models/add-property! :updated-at-timestamped?
+  :insert add-updated-at-timestamp
+  :update add-updated-at-timestamp)
+
 
 ;;; ------------------------------------------------------------ New Permissions Stuff ------------------------------------------------------------
 
diff --git a/src/metabase/models/query.clj b/src/metabase/models/query.clj
new file mode 100644
index 0000000000000000000000000000000000000000..74c80cba809607118cb40df8ac24b4ee3354e5be
--- /dev/null
+++ b/src/metabase/models/query.clj
@@ -0,0 +1,41 @@
+(ns metabase.models.query
+  (:require (toucan [db :as db]
+                    [models :as models])
+            [metabase.db :as mdb]
+            [metabase.util :as u]
+            [metabase.util.honeysql-extensions :as hx]))
+
+(models/defmodel Query :query)
+
+;;; Helper Fns
+
+(defn average-execution-time-ms
+  "Fetch the average execution time (in milliseconds) for query with QUERY-HASH if available.
+   Returns `nil` if no information is available."
+  ^Integer [^bytes query-hash]
+  {:pre [(instance? (Class/forName "[B") query-hash)]}
+  (db/select-one-field :average_execution_time Query :query_hash query-hash))
+
+(defn- int-casting-type
+  "Return appropriate type for use in SQL `CAST(x AS type)` statement.
+   MySQL doesn't accept `integer`, so we have to use `unsigned`; Postgres doesn't accept `unsigned`.
+   so we have to use `integer`. Yay SQL dialect differences :D"
+  []
+  (if (= (mdb/db-type) :mysql)
+    :unsigned
+    :integer))
+
+(defn update-average-execution-time!
+  "Update the recorded average execution time for query with QUERY-HASH."
+  ^Integer [^bytes query-hash, ^Integer execution-time-ms]
+  {:pre [(instance? (Class/forName "[B") query-hash)]}
+  (or
+   ;; if there's already a matching Query update the rolling average
+   (db/update-where! Query {:query_hash query-hash}
+     :average_execution_time (hx/cast (int-casting-type) (hx/round (hx/+ (hx/* 0.9 :average_execution_time)
+                                                                         (*    0.1 execution-time-ms))
+                                                                   0)))
+   ;; otherwise add a new entry, using the value of EXECUTION-TIME-MS as a starting point
+   (db/insert! Query
+     :query_hash             query-hash
+     :average_execution_time execution-time-ms)))
diff --git a/src/metabase/models/query_cache.clj b/src/metabase/models/query_cache.clj
new file mode 100644
index 0000000000000000000000000000000000000000..e4277919d641cb58629f75311f2da3093c13794a
--- /dev/null
+++ b/src/metabase/models/query_cache.clj
@@ -0,0 +1,12 @@
+(ns metabase.models.query-cache
+  "A model used to cache query results in the database."
+  (:require [toucan.models :as models]
+            [metabase.util :as u]))
+
+(models/defmodel QueryCache :query_cache)
+
+(u/strict-extend (class QueryCache)
+  models/IModel
+  (merge models/IModelDefaults
+         {:types      (constantly {:results :compressed})
+          :properties (constantly {:updated-at-timestamped? true})}))
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 970f490a27efab049bce36401c38a49df17a718a..713a2b92f49c6ae73399e1fd6b0d55835e7aaaca 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -53,7 +53,7 @@
 
 
 (def ^:private Type
-  (s/enum :string :boolean :json))
+  (s/enum :string :boolean :json :integer))
 
 (def ^:private SettingDefinition
   {:name        s/Keyword
@@ -152,6 +152,12 @@
   ^Boolean [setting-or-name]
   (string->boolean (get-string setting-or-name)))
 
+(defn get-integer
+  "Get integer value of (presumably `:integer`) SETTING-OR-NAME. This is the default getter for `:integer` settings."
+  ^Integer [setting-or-name]
+  (when-let [s (get-string setting-or-name)]
+    (Integer/parseInt s)))
+
 (defn get-json
   "Get the string value of SETTING-OR-NAME and parse it as JSON."
   [setting-or-name]
@@ -160,6 +166,7 @@
 (def ^:private default-getter-for-type
   {:string  get-string
    :boolean get-boolean
+   :integer get-integer
    :json    get-json})
 
 (defn get
@@ -220,6 +227,15 @@
                                    false "false"
                                    nil   nil))))
 
+(defn set-integer!
+  "Set the value of integer SETTING-OR-NAME."
+  [setting-or-name new-value]
+  (set-string! setting-or-name (when new-value
+                                 (assert (or (integer? new-value)
+                                             (and (string? new-value)
+                                                  (re-matches #"^\d+$" new-value))))
+                                 (str new-value))))
+
 (defn set-json!
   "Serialize NEW-VALUE for SETTING-OR-NAME as a JSON string and save it."
   [setting-or-name new-value]
@@ -229,6 +245,7 @@
 (def ^:private default-setter-for-type
   {:string  set-string!
    :boolean set-boolean!
+   :integer set-integer!
    :json    set-json!})
 
 (defn set!
@@ -316,7 +333,7 @@
    You may optionally pass any of the OPTIONS below:
 
    *  `:default` - The default value of the setting. (default: `nil`)
-   *  `:type` - `:string` (default), `:boolean`, or `:json`. Non-`:string` settings have special default getters and setters that automatically coerce values to the correct types.
+   *  `:type` - `:string` (default), `:boolean`, `:integer`, or `:json`. Non-`:string` settings have special default getters and setters that automatically coerce values to the correct types.
    *  `:internal?` - This `Setting` is for internal use and shouldn't be exposed in the UI (i.e., not
                      returned by the corresponding endpoints). Default: `false`
    *  `:getter` - A custom getter fn, which takes no arguments. Overrides the default implementation.
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 107170404c3abdb8e97b213e3541995cc9523398..4d9900614fa575d603f9838e7abeefbc22074568 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -53,6 +53,40 @@
   :type    :boolean
   :default false)
 
+
+(defsetting enable-query-caching
+  "Enabling caching will save the results of queries that take a long time to run."
+  :type    :boolean
+  :default false)
+
+(defsetting query-caching-max-kb
+  "The maximum size of the cache per card, in kilobytes:"
+  ;; (This size is a measurement of the length of *uncompressed* serialized result *rows*. The actual size of
+  ;; the results as stored will vary somewhat, since this measurement doesn't include metadata returned with the
+  ;; results, and doesn't consider whether the results are compressed, as the `:db` backend does.)
+  :type    :integer
+  :default 1000)
+
+(defsetting query-caching-max-ttl
+  "The absoulte maximum time to keep any cached query results, in seconds."
+  :type    :integer
+  :default (* 60 60 24 100)) ; 100 days
+
+(defsetting query-caching-min-ttl
+  "Metabase will cache all saved questions with an average query execution time longer than
+   this many seconds:"
+  :type    :integer
+  :default 60)
+
+(defsetting query-caching-ttl-ratio
+  "To determine how long each saved question's cached result should stick around, we take the
+   query's average execution time and multiply that by whatever you input here. So if a query
+   takes on average 2 minutes to run, and you input 10 for your multiplier, its cache entry
+   will persist for 20 minutes."
+  :type    :integer
+  :default 10)
+
+
 (defn remove-public-uuid-if-public-sharing-is-disabled
   "If public sharing is *disabled* and OBJECT has a `:public_uuid`, remove it so people don't try to use it (since it won't work).
    Intended for use as part of a `post-select` implementation for Cards and Dashboards."
@@ -81,6 +115,7 @@
    :anon_tracking_enabled (anon-tracking-enabled)
    :custom_geojson        (setting/get :custom-geojson)
    :email_configured      ((resolve 'metabase.email/email-configured?))
+   :enable_query_caching  (enable-query-caching)
    :engines               ((resolve 'metabase.driver/available-drivers))
    :ga_code               "UA-60817802-1"
    :google_auth_client_id (setting/get :google-auth-client-id)
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index f08dfa5b4bee3857c06b897dce41b4a81b8fcf7b..834d312848664a35d1e38417a565cc2ed27cb2cf 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -4,13 +4,15 @@
             [schema.core :as s]
             [toucan.db :as db]
             [metabase.driver :as driver]
-            [metabase.models.query-execution :refer [QueryExecution], :as query-execution]
+            (metabase.models [query :as query]
+                             [query-execution :refer [QueryExecution], :as query-execution])
             [metabase.query-processor.util :as qputil]
             (metabase.query-processor.middleware [add-implicit-clauses :as implicit-clauses]
                                                  [add-row-count-and-status :as row-count-and-status]
                                                  [add-settings :as add-settings]
                                                  [annotate-and-sort :as annotate-and-sort]
                                                  [catch-exceptions :as catch-exceptions]
+                                                 [cache :as cache]
                                                  [cumulative-aggregations :as cumulative-ags]
                                                  [dev :as dev]
                                                  [driver-specific :as driver-specific]
@@ -79,8 +81,9 @@
        driver-specific/process-query-in-context      ; (drivers can inject custom middleware if they implement IDriver's `process-query-in-context`)
        add-settings/add-settings
        resolve-driver/resolve-driver                 ; ▲▲▲ DRIVER RESOLUTION POINT ▲▲▲ All functions *above* will have access to the driver during PRE- *and* POST-PROCESSING
-       catch-exceptions/catch-exceptions
-       log-query/log-initial-query)
+       log-query/log-initial-query
+       cache/maybe-return-cached-results
+       catch-exceptions/catch-exceptions)
    query))
 ;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP, e.g. the results of `expand-macros` are (eventually) passed to `expand-resolve`
 
@@ -100,9 +103,10 @@
 ;;; +----------------------------------------------------------------------------------------------------+
 
 (defn- save-query-execution!
-  "Save (or update) a `QueryExecution`."
+  "Save a `QueryExecution` and update the average execution time for the corresponding `Query`."
   [query-execution]
   (u/prog1 query-execution
+    (query/update-average-execution-time! (:hash query-execution) (:running_time query-execution))
     (db/insert! QueryExecution (dissoc query-execution :json_query))))
 
 (defn- save-and-return-failed-query!
@@ -126,17 +130,20 @@
 (defn- save-and-return-successful-query!
   "Save QueryExecution state and construct a completed (successful) query response"
   [query-execution query-result]
-  ;; record our query execution and format response
-  (-> (assoc query-execution
-        :running_time (- (System/currentTimeMillis)
-                         (:start_time_millis query-execution))
-        :result_rows  (get query-result :row_count 0))
-      (dissoc :start_time_millis)
-      save-query-execution!
-      ;; at this point we've saved and we just need to massage things into our final response format
-      (dissoc :error :result_rows :hash :executor_id :native :card_id :dashboard_id :pulse_id)
-      (merge query-result)
-      (assoc :status :completed)))
+  (let [query-execution (-> (assoc query-execution
+                              :running_time (- (System/currentTimeMillis)
+                                               (:start_time_millis query-execution))
+                              :result_rows  (get query-result :row_count 0))
+                            (dissoc :start_time_millis))]
+    ;; only insert a new record into QueryExecution if the results *were not* cached (i.e., only if a Query was actually ran)
+    (when-not (:cached query-result)
+      (save-query-execution! query-execution))
+    ;; ok, now return the results in the normal response format
+    (merge (dissoc query-execution :error :result_rows :hash :executor_id :native :card_id :dashboard_id :pulse_id)
+           query-result
+           {:status                 :completed
+            :average_execution_time (when (:cached query-result)
+                                      (query/average-execution-time-ms (:hash query-execution)))})))
 
 
 (defn- assert-query-status-successful
diff --git a/src/metabase/query_processor/middleware/cache.clj b/src/metabase/query_processor/middleware/cache.clj
new file mode 100644
index 0000000000000000000000000000000000000000..06bea49173d50e81db33bda3737b1238e842219a
--- /dev/null
+++ b/src/metabase/query_processor/middleware/cache.clj
@@ -0,0 +1,146 @@
+(ns metabase.query-processor.middleware.cache
+  "Middleware that returns cached results for queries when applicable.
+
+   If caching is enabled (`enable-query-caching` is `true`) cached results will be returned for Cards if possible. There's
+   a global default TTL defined by the setting `query-caching-default-ttl`, but individual Cards can override this value
+   with custom TTLs with a value for `:cache_ttl`.
+
+   For all other queries, caching is skipped.
+
+   Various caching backends are defined in `metabase.query-processor.middleware.cache-backend` namespaces.
+   The default backend is `db`, which uses the application database; this value can be changed by setting the env var
+   `MB_QP_CACHE_BACKEND`.
+
+    Refer to `metabase.query-processor.middleware.cache-backend.interface` for more details about how the cache backends themselves."
+  (:require [clojure.tools.logging :as log]
+            [metabase.config :as config]
+            [metabase.public-settings :as public-settings]
+            [metabase.query-processor.middleware.cache-backend.interface :as i]
+            [metabase.query-processor.util :as qputil]
+            [metabase.util :as u]))
+
+(def ^:dynamic ^Boolean *ignore-cached-results*
+  "Should we force the query to run, ignoring cached results even if they're available?
+   Setting this to `true` will run the query again and will still save the updated results."
+  false)
+
+
+;;; ------------------------------------------------------------ Backend ------------------------------------------------------------
+
+(def ^:private backend-instance
+  (atom nil))
+
+(defn- valid-backend? [instance] (extends? i/IQueryProcessorCacheBackend (class instance)))
+
+(defn- get-backend-instance-in-namespace
+  "Return a valid query cache backend `instance` in BACKEND-NS-SYMB, or throw an Exception if none exists."
+  ;; if for some reason the resolved var doesn't satisfy `IQueryProcessorCacheBackend` we'll reload the namespace
+  ;; it belongs to and try one more time.
+  ;; This fixes the issue in dev sessions where the interface namespace gets reloaded causing the cache implementation
+  ;; to no longer satisfy the protocol
+  ([backend-ns-symb]
+   (get-backend-instance-in-namespace backend-ns-symb :allow-reload))
+  ([backend-ns-symb allow-reload?]
+   (let [varr (ns-resolve backend-ns-symb 'instance)]
+     (cond
+       (not varr)             (throw (Exception. (str "No var named 'instance' found in namespace " backend-ns-symb)))
+       (valid-backend? @varr) @varr
+       allow-reload?          (do (require backend-ns-symb :reload)
+                                  (get-backend-instance-in-namespace backend-ns-symb false))
+       :else                  (throw (Exception. (format "%s/instance doesn't satisfy IQueryProcessorCacheBackend" backend-ns-symb)))))))
+
+(defn- set-backend!
+  "Set the cache backend to the cache defined by the keyword BACKEND.
+
+   (This should be something like `:db`, `:redis`, or `:memcached`. See the
+   documentation in `metabase.query-processor.middleware.cache-backend.interface` for details on how this works.)"
+  ([]
+   (set-backend! (config/config-kw :mb-qp-cache-backend)))
+  ([backend]
+   (let [backend-ns (symbol (str "metabase.query-processor.middleware.cache-backend." (munge (name backend))))]
+     (require backend-ns)
+     (log/info "Using query processor cache backend:" (u/format-color 'blue backend) (u/emoji "💾"))
+     (reset! backend-instance (get-backend-instance-in-namespace backend-ns)))))
+
+
+
+;;; ------------------------------------------------------------ Cache Operations ------------------------------------------------------------
+
+(defn- cached-results [query-hash max-age-seconds]
+  (when-not *ignore-cached-results*
+    (when-let [results (i/cached-results @backend-instance query-hash max-age-seconds)]
+      (assert (u/is-temporal? (:updated_at results))
+        "cached-results should include an `:updated_at` field containing the date when the query was last ran.")
+      (log/info "Returning cached results for query" (u/emoji "💾"))
+      (assoc results :cached true))))
+
+(defn- save-results!  [query-hash results]
+  (log/info "Caching results for next time for query" (u/emoji "💾"))
+  (i/save-results! @backend-instance query-hash results))
+
+
+;;; ------------------------------------------------------------ Middleware ------------------------------------------------------------
+
+(defn- is-cacheable? ^Boolean [{cache-ttl :cache_ttl}]
+  (boolean (and (public-settings/enable-query-caching)
+                cache-ttl)))
+
+(defn- results-are-below-max-byte-threshold?
+  "Measure the size of the `:rows` in QUERY-RESULTS and see whether they're smaller than `query-caching-max-kb`
+   *before* compression."
+  ^Boolean [{{rows :rows} :data}]
+  (let [max-bytes (* (public-settings/query-caching-max-kb) 1024)]
+    ;; We don't want to serialize the entire result set since that could explode if the query is one that returns a
+    ;; huge number of rows. (We also want to keep `:rows` lazy.)
+    ;; So we'll serialize one row at a time, and keep a running total of bytes; if we pass the `query-caching-max-kb`
+    ;; threshold, we'll fail right away.
+    (loop [total-bytes 0, [row & more] rows]
+      (cond
+        (> total-bytes max-bytes) false
+        (not row)                 true
+        :else                     (recur (+ total-bytes (count (str row)))
+                                         more)))))
+
+(defn- save-results-if-successful! [query-hash results]
+  (when (and (= (:status results) :completed)
+             (or (results-are-below-max-byte-threshold? results)
+                 (log/info "Results are too large to cache." (u/emoji "😫"))))
+    (save-results! query-hash results)))
+
+(defn- run-query-and-save-results-if-successful! [query-hash qp query]
+  (let [start-time-ms (System/currentTimeMillis)
+        results       (qp query)
+        total-time-ms (- (System/currentTimeMillis) start-time-ms)
+        min-ttl-ms    (* (public-settings/query-caching-min-ttl) 1000)]
+    (log/info (format "Query took %d ms to run; miminum for cache eligibility is %d ms" total-time-ms min-ttl-ms))
+    (when (>= total-time-ms min-ttl-ms)
+      (save-results-if-successful! query-hash results))
+    results))
+
+(defn- run-query-with-cache [qp {cache-ttl :cache_ttl, :as query}]
+  (let [query-hash (qputil/query-hash query)]
+    (or (cached-results query-hash cache-ttl)
+        (run-query-and-save-results-if-successful! query-hash qp query))))
+
+
+(defn maybe-return-cached-results
+  "Middleware for caching results of a query if applicable.
+   In order for a query to be eligible for caching:
+
+     *  Caching (the `enable-query-caching` Setting) must be enabled
+     *  The query must pass a `:cache_ttl` value. For Cards, this can be the value of `:cache_ttl`,
+        otherwise falling back to the value of the `query-caching-default-ttl` Setting.
+     *  The query must already be permissions-checked. Since the cache bypasses the normal
+        query processor pipeline, the ad-hoc permissions-checking middleware isn't applied for cached results.
+        (The various `/api/card/` endpoints that make use of caching do `can-read?` checks for the Card *before*
+        running the query, satisfying this requirement.)
+     *  The result *rows* of the query must be less than `query-caching-max-kb` when serialized (before compression)."
+  [qp]
+  ;; choose the caching backend if needed
+  (when-not @backend-instance
+    (set-backend!))
+  ;; ok, now do the normal middleware thing
+  (fn [query]
+    (if-not (is-cacheable? query)
+      (qp query)
+      (run-query-with-cache qp query))))
diff --git a/src/metabase/query_processor/middleware/cache_backend/db.clj b/src/metabase/query_processor/middleware/cache_backend/db.clj
new file mode 100644
index 0000000000000000000000000000000000000000..4964d8a88a83ad027b33f3781bbc0e965e568585
--- /dev/null
+++ b/src/metabase/query_processor/middleware/cache_backend/db.clj
@@ -0,0 +1,42 @@
+(ns metabase.query-processor.middleware.cache-backend.db
+  (:require [toucan.db :as db]
+            (metabase.models [interface :as models]
+                             [query-cache :refer [QueryCache]])
+            [metabase.public-settings :as public-settings]
+            [metabase.query-processor.middleware.cache-backend.interface :as i]
+            [metabase.util :as u]))
+
+(defn- cached-results
+  "Return cached results for QUERY-HASH if they exist and are newer than MAX-AGE-SECONDS."
+  [query-hash max-age-seconds]
+  (when-let [{:keys [results updated_at]} (db/select-one [QueryCache :results :updated_at]
+                                            :query_hash query-hash
+                                            :updated_at [:>= (u/->Timestamp (- (System/currentTimeMillis)
+                                                                               (* 1000 max-age-seconds)))])]
+    (assoc results :updated_at updated_at)))
+
+(defn- purge-old-cache-entries!
+  "Delete any cache entries that are older than the global max age `max-cache-entry-age-seconds` (currently 3 months)."
+  []
+  (db/simple-delete! QueryCache
+    :updated_at [:<= (u/->Timestamp (- (System/currentTimeMillis)
+                                       (* 1000 (public-settings/query-caching-max-ttl))))]))
+
+(defn- save-results!
+  "Save the RESULTS of query with QUERY-HASH, updating an existing QueryCache entry
+  if one already exists, otherwise creating a new entry."
+  [query-hash results]
+  (purge-old-cache-entries!)
+  (or (db/update-where! QueryCache {:query_hash query-hash}
+        :updated_at (u/new-sql-timestamp)
+        :results    (models/compress results)) ; have to manually call these here since Toucan doesn't call type conversion fns for update-where! (yet)
+      (db/insert! QueryCache
+        :query_hash query-hash
+        :results    results))
+  :ok)
+
+(def instance
+  "Implementation of `IQueryProcessorCacheBackend` that uses the database for caching results."
+  (reify i/IQueryProcessorCacheBackend
+    (cached-results [_ query-hash max-age-seconds] (cached-results query-hash max-age-seconds))
+    (save-results!  [_ query-hash results]         (save-results! query-hash results))))
diff --git a/src/metabase/query_processor/middleware/cache_backend/interface.clj b/src/metabase/query_processor/middleware/cache_backend/interface.clj
new file mode 100644
index 0000000000000000000000000000000000000000..4dbf762ca981c6f32ee4cc28fa0d0995f5be2c46
--- /dev/null
+++ b/src/metabase/query_processor/middleware/cache_backend/interface.clj
@@ -0,0 +1,37 @@
+(ns metabase.query-processor.middleware.cache-backend.interface
+  "Interface used to define different Query Processor cache backends.
+   Defining a backend is straightforward: define a new namespace with the pattern
+
+     metabase.query-processor.middleware.cache-backend.<backend>
+
+   Where backend is a key representing the backend, e.g. `db`, `redis`, or `memcached`.
+
+   In that namespace, create an object that reifies (or otherwise implements) `IQueryProcessorCacheBackend`.
+   This object *must* be stored in a var called `instance`.
+
+   That's it. See `metabase.query-processor.middleware.cache-backend.db` for a complete example of how this is done.")
+
+
+(defprotocol IQueryProcessorCacheBackend
+  "Protocol that different Metabase cache backends must implement.
+
+   QUERY-HASH as passed below is a byte-array representing a 256-byte SHA3 hash; encode this as needed for use as a
+   cache entry key. RESULTS are passed (and should be returned) as a Clojure object, and individual backends are free
+   to encode this as appropriate when storing the results. (It's probably not a bad idea to compress the results; this
+   is what the `:db` backend does.)"
+
+  (cached-results [this, query-hash, ^Integer max-age-seconds]
+    "Return cached results for the query with byte array QUERY-HASH if those results are present in the cache and are less
+     than MAX-AGE-SECONDS old. Otherwise, return `nil`.
+
+  This method must also return a Timestamp from when the query was last ran. This must be `assoc`ed with the query results
+  under the key `:updated_at`.
+
+    (cached-results [_ query-hash max-age-seconds]
+      (when-let [[results updated-at] (maybe-fetch-results query-hash max-age-seconds)]
+        (assoc results :updated_at updated-at)))")
+
+  (save-results! [this query-hash results]
+    "Add a cache entry with the RESULTS of running query with byte array QUERY-HASH.
+     This should replace any prior entries for QUERY-HASH and update the cache timestamp to the current system time.
+     (This is also an appropriate point to purge any entries older than the value of the `query-caching-max-ttl` Setting.)"))
diff --git a/src/metabase/query_processor/util.clj b/src/metabase/query_processor/util.clj
index 3846fb546662a6f6e15dd7d0f4ac8dcb01127db0..dec320e4dc655fec44f52390c155f7996624686f 100644
--- a/src/metabase/query_processor/util.clj
+++ b/src/metabase/query_processor/util.clj
@@ -3,8 +3,7 @@
   (:require (buddy.core [codecs :as codecs]
                         [hash :as hash])
             [cheshire.core :as json]
-            [toucan.db :as db]
-            [metabase.models.query-execution :refer [QueryExecution]]))
+            [toucan.db :as db]))
 
 (defn mbql-query?
   "Is the given query an MBQL query?"
@@ -41,7 +40,7 @@
    (This is done so irrelevant info or options that don't affect query results doesn't result in the same query producing different hashes.)"
   [query]
   {:pre [(map? query)]}
-  (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :parameters :constraints])]
+  (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :native :parameters :constraints])]
     (cond-> query
       (empty? constraints) (dissoc :constraints)
       (empty? parameters)  (dissoc :parameters))))
@@ -50,17 +49,3 @@
   "Return a 256-bit SHA3 hash of QUERY as a key for the cache. (This is returned as a byte array.)"
   [query]
   (hash/sha3-256 (json/generate-string (select-keys-for-hashing query))))
-
-
-;;; ------------------------------------------------------------ Historic Duration Info ------------------------------------------------------------
-
-(defn query-average-duration
-  "Return the average running time of QUERY over the last 10 executions in milliseconds.
-   Returns `nil` if there's not available data."
-  ^Float [query]
-  (when-let [running-times (db/select-field :running_time QueryExecution
-                             :hash (query-hash query)
-                             {:order-by [[:started_at :desc]]
-                              :limit    10})]
-    (float (/ (reduce + running-times)
-              (count running-times)))))
diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj
index 66a5ccaef18b7da1b29dd32bd7fce661c6dd6aae..70bfe6de28eaa0af1de1aae991c085fa5f54f685 100644
--- a/src/metabase/util/honeysql_extensions.clj
+++ b/src/metabase/util/honeysql_extensions.clj
@@ -21,6 +21,25 @@
   (intern 'honeysql.format 'quote-fns
           (assoc quote-fns :h2 (comp s/upper-case ansi-quote-fn))))
 
+
+;; `:crate` quote style that correctly quotes nested column identifiers
+(defn- str-insert
+  "Insert C in string S at index I."
+  [s c i]
+  (str c (subs s 0 i) c (subs s i)))
+
+(defn- crate-column-identifier
+  [^CharSequence s]
+  (let [idx (s/index-of s "[")]
+    (if (nil? idx)
+      (str \" s \")
+      (str-insert s "\"" idx))))
+
+(let [quote-fns @(resolve 'honeysql.format/quote-fns)]
+  (intern 'honeysql.format 'quote-fns
+          (assoc quote-fns :crate crate-column-identifier)))
+
+
 ;; register the `extract` function with HoneySQL
 ;; (hsql/format (hsql/call :extract :a :b)) -> "extract(a from b)"
 (defmethod hformat/fn-handler "extract" [_ unit expr]
@@ -92,10 +111,15 @@
   (hsql/call :cast x (hsql/raw (name c))))
 
 (defn format
-  "SQL `FORMAT` function."
+  "SQL `format` function."
   [format-str expr]
   (hsql/call :format expr (literal format-str)))
 
+(defn round
+  "SQL `round` function."
+  [x decimal-places]
+  (hsql/call :round x decimal-places))
+
 (defn ->date                     "CAST X to a `date`."                     [x] (cast :date x))
 (defn ->datetime                 "CAST X to a `datetime`."                 [x] (cast :datetime x))
 (defn ->timestamp                "CAST X to a `timestamp`."                [x] (cast :timestamp x))
diff --git a/src/metabase/util/urls.clj b/src/metabase/util/urls.clj
index db1c2948f50289fd71a4222729b61d59259f920c..3c27b5ee2033afecfc12bad23d45101565f0e1e0 100644
--- a/src/metabase/util/urls.clj
+++ b/src/metabase/util/urls.clj
@@ -18,16 +18,16 @@
 (defn dashboard-url
   "Return an appropriate URL for a `Dashboard` with ID.
 
-     (dashboard-url 10) -> \"http://localhost:3000/dash/10\""
+     (dashboard-url 10) -> \"http://localhost:3000/dashboard/10\""
   [^Integer id]
-  (format "%s/dash/%d" (public-settings/site-url) id))
+  (format "%s/dashboard/%d" (public-settings/site-url) id))
 
 (defn card-url
   "Return an appropriate URL for a `Card` with ID.
 
-     (card-url 10) -> \"http://localhost:3000/card/10\""
+     (card-url 10) -> \"http://localhost:3000/question/10\""
   [^Integer id]
-  (format "%s/card/%d" (public-settings/site-url) id))
+  (format "%s/question/%d" (public-settings/site-url) id))
 
 (defn segment-url
   "Return an appropriate URL for a `Segment` with ID.
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index 34a759795d2831efa8e133687e49cebdbeaf7780..a42630ea44f06bb291c5756a1f8802529f291577 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -36,7 +36,8 @@
    :embedding_params  nil
    :made_public_by_id nil
    :public_uuid       nil
-   :query_type        "query"})
+   :query_type        "query"
+   :cache_ttl         nil})
 
 ;; ## GET /api/card
 ;; Filter cards by database
@@ -246,6 +247,20 @@
        (set-archived! true)
        (set-archived! false)])))
 
+;; Can we clear the description of a Card? (#4738)
+(expect
+  nil
+  (with-temp-card [card {:description "What a nice Card"}]
+    ((user->client :rasta) :put 200 (str "card/" (u/get-id card)) {:description nil})
+    (db/select-one-field :description Card :id (u/get-id card))))
+
+;; description should be blankable as well
+(expect
+  ""
+  (with-temp-card [card {:description "What a nice Card"}]
+    ((user->client :rasta) :put 200 (str "card/" (u/get-id card)) {:description ""})
+    (db/select-one-field :description Card :id (u/get-id card))))
+
 ;; Can we update a card's embedding_params?
 (expect
   {:abc "enabled"}
@@ -513,6 +528,22 @@
   "Not found."
   ((user->client :rasta) :get 404 "card/" :collection :some_fake_collection_slug))
 
+;; Make sure GET /api/card?collection=<slug> still works with Collections with URL-encoded Slugs (#4535)
+(expect
+  []
+  (tt/with-temp Collection [collection {:name "Obsługa klienta"}]
+    (do
+      (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection)
+      ((user->client :rasta) :get 200 "card/" :collection "obs%C5%82uga_klienta"))))
+
+;; ...even if the slug isn't passed in URL-encoded
+(expect
+  []
+  (tt/with-temp Collection [collection {:name "Obsługa klienta"}]
+    (do
+      (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection)
+      ((user->client :rasta) :get 200 "card/" :collection "obsługa_klienta"))))
+
 
 ;;; ------------------------------------------------------------ Bulk Collections Update (POST /api/card/collections) ------------------------------------------------------------
 
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 5ee7f99801c38c1b1de019ed090be47362e6cd41..3020f1edddba5fbd21ffe2a5a0ffcbe17e3a2bcb 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -159,6 +159,19 @@
                                                                                                :creator_id   (user->id :trashbird)})
                               (Dashboard dashboard-id)])))
 
+;; Can we clear the description of a Dashboard? (#4738)
+(expect
+  nil
+  (tt/with-temp Dashboard [dashboard {:description "What a nice Dashboard"}]
+    ((user->client :rasta) :put 200 (str "dashboard/" (u/get-id dashboard)) {:description nil})
+    (db/select-one-field :description Dashboard :id (u/get-id dashboard))))
+
+(expect
+  ""
+  (tt/with-temp Dashboard [dashboard {:description "What a nice Dashboard"}]
+    ((user->client :rasta) :put 200 (str "dashboard/" (u/get-id dashboard)) {:description ""})
+    (db/select-one-field :description Dashboard :id (u/get-id dashboard))))
+
 
 ;; ## DELETE /api/dashboard/:id
 (expect
diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj
index 20a86830f56f070d9df5a9b72ec7eb20930ce132..637acfad10827deecd13941e0ba37f3ff514f5b3 100644
--- a/test/metabase/api/dataset_test.clj
+++ b/test/metabase/api/dataset_test.clj
@@ -52,22 +52,23 @@
 ;; Just a basic sanity check to make sure Query Processor endpoint is still working correctly.
 (expect
   [;; API call response
-   {:data         {:rows    [[1000]]
-                   :columns ["count"]
-                   :cols    [{:base_type "type/Integer", :special_type "type/Number", :name "count", :display_name "count", :id nil, :table_id nil,
-                              :description nil, :target nil, :extra_info {}, :source "aggregation"}]
-                   :native_form true}
-    :row_count    1
-    :status       "completed"
-    :context      "ad-hoc"
-    :json_query   (-> (wrap-inner-query
-                        (query checkins
-                          (ql/aggregation (ql/count))))
-                      (assoc :type "query")
-                      (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}])
-                      (assoc :constraints default-query-constraints))
-    :started_at   true
-    :running_time true}
+   {:data                   {:rows    [[1000]]
+                             :columns ["count"]
+                             :cols    [{:base_type "type/Integer", :special_type "type/Number", :name "count", :display_name "count", :id nil, :table_id nil,
+                                        :description nil, :target nil, :extra_info {}, :source "aggregation"}]
+                             :native_form true}
+    :row_count              1
+    :status                 "completed"
+    :context                "ad-hoc"
+    :json_query             (-> (wrap-inner-query
+                                  (query checkins
+                                    (ql/aggregation (ql/count))))
+                                (assoc :type "query")
+                                (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}])
+                                (assoc :constraints default-query-constraints))
+    :started_at             true
+    :running_time           true
+    :average_execution_time nil}
    ;; QueryExecution record in the DB
    {:hash         true
     :row_count    1
diff --git a/test/metabase/cmd/load_from_h2_test.clj b/test/metabase/cmd/load_from_h2_test.clj
index c9a6c119457d11dc399285b187257b64bdddf860..3d3942f4773bce5f0f0b16c0e2fcd3145abd288e 100644
--- a/test/metabase/cmd/load_from_h2_test.clj
+++ b/test/metabase/cmd/load_from_h2_test.clj
@@ -13,7 +13,10 @@
 
 (def ^:private models-to-exclude
   "Models that should *not* be migrated in `load-from-h2`."
-  #{"LegacyQueryExecution"})
+  #{"LegacyQueryExecution"
+    "Query"
+    "QueryCache"
+    "QueryExecution"})
 
 (defn- all-model-names []
   (set (for [ns       (ns-find/find-namespaces (classpath/classpath))
diff --git a/test/metabase/driver/bigquery_test.clj b/test/metabase/driver/bigquery_test.clj
index d6767253f02cc9514439c1ae831eef331aeb3370..976bf1d3410d88076d6b5c1b41f58b063ec2f709 100644
--- a/test/metabase/driver/bigquery_test.clj
+++ b/test/metabase/driver/bigquery_test.clj
@@ -1,10 +1,14 @@
 (ns metabase.driver.bigquery-test
-  (:require metabase.driver.bigquery
+  (:require [expectations :refer :all]
+            metabase.driver.bigquery
             [metabase.models.database :as database]
             [metabase.query-processor :as qp]
+            [metabase.query-processor.expand :as ql]
+            [metabase.query-processor-test :as qptest]
             [metabase.test.data :as data]
             (metabase.test.data [datasets :refer [expect-with-engine]]
-                                [interface :refer [def-database-definition]])))
+                                [interface :refer [def-database-definition]])
+            [metabase.test.util :as tu]))
 
 
 ;; Test native queries
@@ -29,3 +33,58 @@
                                          :type     :native
                                          :database (data/id)}))
                [:cols :columns]))
+
+;; make sure that the bigquery driver can handle named columns with characters that aren't allowed in BQ itself
+(expect-with-engine :bigquery
+  {:rows    [[113]]
+   :columns ["User_ID_Plus_Venue_ID"]}
+  (qptest/rows+column-names
+    (qp/process-query {:database (data/id)
+                       :type     "query"
+                       :query    {:source_table (data/id :checkins)
+                                  :aggregation  [["named" ["max" ["+" ["field-id" (data/id :checkins :user_id)]
+                                                                      ["field-id" (data/id :checkins :venue_id)]]]
+                                                  "User ID Plus Venue ID"]]}})))
+
+;; make sure BigQuery can handle two aggregations with the same name (#4089)
+(tu/resolve-private-vars metabase.driver.bigquery
+  deduplicate-aliases update-select-subclause-aliases)
+
+(expect
+  ["sum" "count" "sum_2" "avg" "sum_3" "min"]
+  (deduplicate-aliases ["sum" "count" "sum" "avg" "sum" "min"]))
+
+(expect
+  ["sum" "count" "sum_2" "avg" "sum_2_2" "min"]
+  (deduplicate-aliases ["sum" "count" "sum" "avg" "sum_2" "min"]))
+
+(expect
+  ["sum" "count" nil "sum_2"]
+  (deduplicate-aliases ["sum" "count" nil "sum"]))
+
+(expect
+  [[:user_id "user_id_2"] :venue_id]
+  (update-select-subclause-aliases [[:user_id "user_id"] :venue_id]
+                                   ["user_id_2" nil]))
+
+
+(expect-with-engine :bigquery
+  {:rows [[7929 7929]], :columns ["sum" "sum_2"]}
+  (qptest/rows+column-names
+    (qp/process-query {:database (data/id)
+                       :type     "query"
+                       :query    (-> {}
+                                     (ql/source-table (data/id :checkins))
+                                     (ql/aggregation (ql/sum (ql/field-id (data/id :checkins :user_id)))
+                                                     (ql/sum (ql/field-id (data/id :checkins :user_id)))))})))
+
+(expect-with-engine :bigquery
+  {:rows [[7929 7929 7929]], :columns ["sum" "sum_2" "sum_3"]}
+  (qptest/rows+column-names
+    (qp/process-query {:database (data/id)
+                       :type     "query"
+                       :query    (-> {}
+                                     (ql/source-table (data/id :checkins))
+                                     (ql/aggregation (ql/sum (ql/field-id (data/id :checkins :user_id)))
+                                                     (ql/sum (ql/field-id (data/id :checkins :user_id)))
+                                                     (ql/sum (ql/field-id (data/id :checkins :user_id)))))})))
diff --git a/test/metabase/driver/generic_sql/util/unprepare_test.clj b/test/metabase/driver/generic_sql/util/unprepare_test.clj
index b20a4d91e9bbf2cad244e8258bd19c5de1c573de..ce98bb08df85eeff569c6943d0b1a9155d57d096 100644
--- a/test/metabase/driver/generic_sql/util/unprepare_test.clj
+++ b/test/metabase/driver/generic_sql/util/unprepare_test.clj
@@ -8,3 +8,12 @@
                         "Cam's Cool Toucan"
                         true
                         #inst "2017-01-01T00:00:00.000Z"]))
+
+(expect
+  "SELECT 'Cam''s Cool Toucan' FROM TRUE WHERE x ?? y AND z = from_iso8601_timestamp('2017-01-01T00:00:00.000Z')"
+  (unprepare/unprepare ["SELECT ? FROM ? WHERE x ?? y AND z = ?"
+                        "Cam's Cool Toucan"
+                        true
+                        #inst "2017-01-01T00:00:00.000Z"]
+                       :quote-escape "'"
+                       :iso-8601-fn  :from_iso8601_timestamp))
diff --git a/test/metabase/driver/generic_sql_test.clj b/test/metabase/driver/generic_sql_test.clj
index bbb68a74743a7d5775c342a05c8827bc168405f3..4c51952cf9e49066ce8c3f5276433e1511db5218 100644
--- a/test/metabase/driver/generic_sql_test.clj
+++ b/test/metabase/driver/generic_sql_test.clj
@@ -19,7 +19,7 @@
 (def ^:private generic-sql-engines
   (delay (set (for [engine datasets/all-valid-engines
                     :let   [driver (driver/engine->driver engine)]
-                    :when  (not= engine :bigquery)                                       ; bigquery doesn't use the generic sql implementations of things like `field-avg-length`
+                    :when  (not (contains? #{:bigquery :presto} engine))                 ; bigquery and presto don't use the generic sql implementations of things like `field-avg-length`
                     :when  (extends? ISQLDriver (class driver))]
                 (do (require (symbol (str "metabase.test.data." (name engine))) :reload) ; otherwise it gets all snippy if you try to do `lein test metabase.driver.generic-sql-test`
                     engine)))))
diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj
index 3332a298277eff2370b30b79f5f3f47781186cdc..1387e674bc6939b26d293749344eb763b1c821f4 100644
--- a/test/metabase/driver/mongo_test.clj
+++ b/test/metabase/driver/mongo_test.clj
@@ -175,21 +175,33 @@
             (ql/filter (ql/= $bird_id "abcdefabcdefabcdefabcdef"))))))
 
 
-;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" forms (#3741) ------------------------------------------------------------
+;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" and "ObjectId(...) forms (#3741, #4448) ------------------------------------------------------------
 (tu/resolve-private-vars metabase.driver.mongo.query-processor
-  maybe-decode-iso-date-fncall decode-iso-date-fncalls encode-iso-date-fncalls)
+  maybe-decode-fncall decode-fncalls encode-fncalls)
 
 (expect
   "[{\"$match\":{\"date\":{\"$gte\":[\"___ISODate\", \"2012-01-01\"]}}}]"
-  (encode-iso-date-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]"))
+  (encode-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]"))
+
+(expect
+  "[{\"$match\":{\"entityId\":{\"$eq\":[\"___ObjectId\", \"583327789137b2700a1621fb\"]}}}]"
+  (encode-fncalls "[{\"$match\":{\"entityId\":{\"$eq\":ObjectId(\"583327789137b2700a1621fb\")}}}]"))
 
 (expect
   (DateTime. "2012-01-01")
-  (maybe-decode-iso-date-fncall ["___ISODate" "2012-01-01"]))
+  (maybe-decode-fncall ["___ISODate" "2012-01-01"]))
+
+(expect
+  (ObjectId. "583327789137b2700a1621fb")
+  (maybe-decode-fncall ["___ObjectId" "583327789137b2700a1621fb"]))
 
 (expect
   [{:$match {:date {:$gte (DateTime. "2012-01-01")}}}]
-  (decode-iso-date-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}]))
+  (decode-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}]))
+
+(expect
+  [{:$match {:entityId {:$eq (ObjectId. "583327789137b2700a1621fb")}}}]
+  (decode-fncalls [{:$match {:entityId {:$eq ["___ObjectId" "583327789137b2700a1621fb"]}}}]))
 
 (datasets/expect-with-engine :mongo
   5
@@ -197,3 +209,11 @@
                                              :collection "checkins"}
                                   :type     :native
                                   :database (data/id)}))))
+
+(datasets/expect-with-engine :mongo
+  0
+  ;; this query shouldn't match anything, so we're just checking that it completes successfully
+  (count (rows (qp/process-query {:native   {:query      "[{\"$match\": {\"_id\": {\"$eq\": ObjectId(\"583327789137b2700a1621fb\")}}}]"
+                                             :collection "venues"}
+                                  :type     :native
+                                  :database (data/id)}))))
diff --git a/test/metabase/driver/presto_test.clj b/test/metabase/driver/presto_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..86502b962ef26390c1e6f3ae9c777b69409dc94d
--- /dev/null
+++ b/test/metabase/driver/presto_test.clj
@@ -0,0 +1,143 @@
+(ns metabase.driver.presto-test
+  (:require [expectations :refer :all]
+            [toucan.db :as db]
+            [metabase.driver :as driver]
+            [metabase.driver.generic-sql :as sql]
+            [metabase.models.table :as table]
+            [metabase.test.data :as data]
+            [metabase.test.data.datasets :as datasets]
+            [metabase.test.util :refer [resolve-private-vars]])
+  (:import (metabase.driver.presto PrestoDriver)))
+
+(resolve-private-vars metabase.driver.presto details->uri details->request parse-presto-results quote-name quote+combine-names apply-page)
+
+;;; HELPERS
+
+(expect
+  "http://localhost:8080/"
+  (details->uri {:host "localhost", :port 8080, :ssl false} "/"))
+
+(expect
+  "https://localhost:8443/"
+  (details->uri {:host "localhost", :port 8443, :ssl true} "/"))
+
+(expect
+  "http://localhost:8080/v1/statement"
+  (details->uri {:host "localhost", :port 8080, :ssl false} "/v1/statement"))
+
+(expect
+  {:headers {"X-Presto-Source" "metabase"
+             "X-Presto-User"   "user"}}
+  (details->request {:user "user"}))
+
+(expect
+  {:headers    {"X-Presto-Source" "metabase"
+                "X-Presto-User"   "user"}
+   :basic-auth ["user" "test"]}
+  (details->request {:user "user", :password "test"}))
+
+(expect
+  {:headers {"X-Presto-Source"    "metabase"
+             "X-Presto-User"      "user"
+             "X-Presto-Catalog"   "test_data"
+             "X-Presto-Time-Zone" "America/Toronto"}}
+  (details->request {:user "user", :catalog "test_data", :report-timezone "America/Toronto"}))
+
+(expect
+  [["2017-04-03"
+    #inst "2017-04-03T14:19:17.417000000-00:00"
+    #inst "2017-04-03T10:19:17.417000000-00:00"
+    3.1416M
+    "test"]]
+  (parse-presto-results [{:type "date"} {:type "timestamp with time zone"} {:type "timestamp"} {:type "decimal(10,4)"} {:type "varchar(255)"}]
+                        [["2017-04-03", "2017-04-03 10:19:17.417 America/Toronto", "2017-04-03 10:19:17.417", "3.1416", "test"]]))
+
+(expect
+  "\"weird.table\"\" name\""
+  (quote-name "weird.table\" name"))
+
+(expect
+  "\"weird . \"\"schema\".\"weird.table\"\" name\""
+  (quote+combine-names "weird . \"schema" "weird.table\" name"))
+
+;; DESCRIBE-DATABASE
+(datasets/expect-with-engine :presto
+  {:tables #{{:name "categories" :schema "default"}
+             {:name "venues"     :schema "default"}
+             {:name "checkins"   :schema "default"}
+             {:name "users"      :schema "default"}}}
+  (driver/describe-database (PrestoDriver.) (data/db)))
+
+;; DESCRIBE-TABLE
+(datasets/expect-with-engine :presto
+  {:name   "venues"
+   :schema "default"
+   :fields #{{:name      "name",
+              :base-type :type/Text}
+             {:name      "latitude"
+              :base-type :type/Float}
+             {:name      "longitude"
+              :base-type :type/Float}
+             {:name      "price"
+              :base-type :type/Integer}
+             {:name      "category_id"
+              :base-type :type/Integer}
+             {:name      "id"
+              :base-type :type/Integer}}}
+  (driver/describe-table (PrestoDriver.) (data/db) (db/select-one 'Table :id (data/id :venues))))
+
+;;; ANALYZE-TABLE
+(datasets/expect-with-engine :presto
+  {:row_count 100
+   :fields    [{:id (data/id :venues :category_id), :values [2 3 4 5 6 7 10 11 12 13 14 15 18 19 20 29 40 43 44 46 48 49 50 58 64 67 71 74]}
+               {:id (data/id :venues :id)}
+               {:id (data/id :venues :latitude)}
+               {:id (data/id :venues :longitude)}
+               {:id (data/id :venues :name), :values (db/select-one-field :values 'FieldValues, :field_id (data/id :venues :name))}
+               {:id (data/id :venues :price), :values [1 2 3 4]}]}
+  (let [venues-table (db/select-one 'Table :id (data/id :venues))]
+    (driver/analyze-table (PrestoDriver.) venues-table (set (mapv :id (table/fields venues-table))))))
+
+;;; FIELD-VALUES-LAZY-SEQ
+(datasets/expect-with-engine :presto
+  ["Red Medicine"
+   "Stout Burgers & Beers"
+   "The Apple Pan"
+   "Wurstküche"
+   "Brite Spot Family Restaurant"]
+  (take 5 (driver/field-values-lazy-seq (PrestoDriver.) (db/select-one 'Field :id (data/id :venues :name)))))
+
+;;; TABLE-ROWS-SEQ
+(datasets/expect-with-engine :presto
+  [{:name "Red Medicine",                 :price 3, :category_id  4, :id 1}
+   {:name "Stout Burgers & Beers",        :price 2, :category_id 11, :id 2}
+   {:name "The Apple Pan",                :price 2, :category_id 11, :id 3}
+   {:name "Wurstküche",                   :price 2, :category_id 29, :id 4}
+   {:name "Brite Spot Family Restaurant", :price 2, :category_id 20, :id 5}]
+  (for [row (take 5 (sort-by :id (driver/table-rows-seq (PrestoDriver.)
+                                                        (db/select-one 'Database :id (data/id))
+                                                        (db/select-one 'RawTable :id (db/select-one-field :raw_table_id 'Table, :id (data/id :venues))))))]
+    (-> (dissoc row :latitude :longitude)
+        (update :price int)
+        (update :category_id int)
+        (update :id int))))
+
+;;; FIELD-PERCENT-URLS
+(datasets/expect-with-engine :presto
+  0.5
+  (data/dataset half-valid-urls
+    (sql/field-percent-urls (PrestoDriver.) (db/select-one 'Field :id (data/id :urls :url)))))
+
+;;; APPLY-PAGE
+(expect
+  {:select ["name" "id"]
+   :from   [{:select   [[:default.categories.name "name"] [:default.categories.id "id"] [{:s "row_number() OVER (ORDER BY \"default\".\"categories\".\"id\" ASC)"} :__rownum__]]
+             :from     [:default.categories]
+             :order-by [[:default.categories.id :asc]]}]
+   :where  [:> :__rownum__ 5]
+   :limit  5}
+  (apply-page {:select   [[:default.categories.name "name"] [:default.categories.id "id"]]
+               :from     [:default.categories]
+               :order-by [[:default.categories.id :asc]]}
+              {:page {:page  2
+                      :items 5}}))
diff --git a/test/metabase/events/revision_test.clj b/test/metabase/events/revision_test.clj
index 72c08795905261c829981144aba90e986bdc4abc..ef56ae4507f773109b1879a3d40dd5d45ef6f5bc 100644
--- a/test/metabase/events/revision_test.clj
+++ b/test/metabase/events/revision_test.clj
@@ -41,6 +41,7 @@
    :made_public_by_id      nil
    :name                   (:name card)
    :public_uuid            nil
+   :cache_ttl              nil
    :query_type             "query"
    :table_id               (id :categories)
    :visualization_settings {}})
diff --git a/test/metabase/permissions_collection_test.clj b/test/metabase/permissions_collection_test.clj
index a4d86a18d98bf5b22125afd3990a91eb20302c4a..d6e82ec37120919812faf7c81eb07854a1964be5 100644
--- a/test/metabase/permissions_collection_test.clj
+++ b/test/metabase/permissions_collection_test.clj
@@ -19,9 +19,9 @@
 (defn- api-call-was-successful? {:style/indent 0} [response]
   (when (and (string? response)
              (not= response "You don't have permissions to do that."))
-    (println "users in db:" (db/select-field :email 'User)) ; NOCOMMIT
     (println "RESPONSE:" response)) ; DEBUG
-  (not= response "You don't have permissions to do that."))
+  (and (not= response "You don't have permissions to do that.")
+       (not= response "Unauthenticated")))
 
 (defn- can-run-query? [username]
   (api-call-was-successful? ((test-users/user->client username) :post (format "card/%d/query" (u/get-id *card:db2-count-of-venues*)))))
@@ -52,9 +52,12 @@
 (perms-test/expect-with-test-data
   true
   (tt/with-temp Collection [collection]
+    (println "[In the occasionally failing test]") ; DEBUG
     (set-card-collection! collection)
     (permissions/grant-collection-read-permissions! (group/all-users) collection)
-    (can-run-query? :rasta)))
+    ;; try it twice because sometimes it randomly fails :unamused:
+    (or (can-run-query? :rasta)
+        (can-run-query? :rasta))))
 
 ;; Make sure a User isn't allowed to save a Card they have collections readwrite permissions for
 ;; if they don't have data perms for the query
diff --git a/test/metabase/query_processor/middleware/cache_test.clj b/test/metabase/query_processor/middleware/cache_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..9a2f3afe91e5fcb54beedccd69ec417579ac5ef2
--- /dev/null
+++ b/test/metabase/query_processor/middleware/cache_test.clj
@@ -0,0 +1,187 @@
+(ns metabase.query-processor.middleware.cache-test
+  "Tests for the Query Processor cache."
+  (:require [expectations :refer :all]
+            [toucan.db :as db]
+            [metabase.models.query-cache :refer [QueryCache]]
+            [metabase.query-processor.middleware.cache :as cache]
+            [metabase.test.util :as tu]))
+
+(tu/resolve-private-vars metabase.query-processor.middleware.cache
+  is-cacheable?
+  results-are-below-max-byte-threshold?)
+
+(def ^:private mock-results
+  {:row_count 8
+   :status    :completed
+   :data      {:rows [[:toucan      71]
+                      [:bald-eagle  92]
+                      [:hummingbird 11]
+                      [:owl         10]
+                      [:chicken     69]
+                      [:robin       96]
+                      [:osprey      72]
+                      [:flamingo    70]]}})
+
+(def ^:private ^:dynamic ^Integer *query-execution-delay-ms* 0)
+
+(defn- mock-qp [& _]
+  (Thread/sleep *query-execution-delay-ms*)
+  mock-results)
+
+(def ^:private maybe-return-cached-results (cache/maybe-return-cached-results mock-qp))
+
+(defn- clear-cache! [] (db/simple-delete! QueryCache))
+
+(defn- cached? [results]
+  (if (:cached results)
+    :cached
+    :not-cached))
+
+(defn- run-query [& {:as query-kvs}]
+  (cached? (maybe-return-cached-results (merge {:cache_ttl 60, :query :abc} query-kvs))))
+
+
+;;; ------------------------------------------------------------ tests for is-cacheable? ------------------------------------------------------------
+
+;; something is-cacheable? if it includes a cach_ttl and the caching setting is enabled
+(expect
+  (tu/with-temporary-setting-values [enable-query-caching true]
+    (is-cacheable? {:cache_ttl 100})))
+
+(expect
+  false
+  (tu/with-temporary-setting-values [enable-query-caching false]
+    (is-cacheable? {:cache_ttl 100})))
+
+(expect
+  false
+  (tu/with-temporary-setting-values [enable-query-caching true]
+    (is-cacheable? {:cache_ttl nil})))
+
+
+;;; ------------------------------------------------------------ results-are-below-max-byte-threshold? ------------------------------------------------------------
+
+(expect
+  (tu/with-temporary-setting-values [query-caching-max-kb 128]
+    (results-are-below-max-byte-threshold? {:data {:rows [[1 "ABCDEF"]
+                                                          [3 "GHIJKL"]]}})))
+
+(expect
+  false
+  (tu/with-temporary-setting-values [query-caching-max-kb 1]
+    (results-are-below-max-byte-threshold? {:data {:rows (repeat 500 [1 "ABCDEF"])}})))
+
+;; check that `results-are-below-max-byte-threshold?` is lazy and fails fast if the query is over the threshold rather than serializing the entire thing
+(expect
+  false
+  (let [lazy-seq-realized? (atom false)]
+    (tu/with-temporary-setting-values [query-caching-max-kb 1]
+      (results-are-below-max-byte-threshold? {:data {:rows (lazy-cat (repeat 500 [1 "ABCDEF"])
+                                                                     (do (reset! lazy-seq-realized? true)
+                                                                         [2 "GHIJKL"]))}})
+      @lazy-seq-realized?)))
+
+
+;;; ------------------------------------------------------------ End-to-end middleware tests ------------------------------------------------------------
+
+;; if there's nothing in the cache, cached results should *not* be returned
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)))
+
+;; if we run the query twice, the second run should return cached results
+(expect
+  :cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (run-query)))
+
+;; ...but if the cache entry is past it's TTL, the cached results shouldn't be returned
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query :cache_ttl 1)
+    (Thread/sleep 2000)
+    (run-query :cache_ttl 1)))
+
+;; if caching is disabled then cache shouldn't be used even if there's something valid in there
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (tu/with-temporary-setting-values [enable-query-caching  false
+                                       query-caching-min-ttl 0]
+      (run-query))))
+
+
+;; check that `query-caching-max-kb` is respected and queries aren't cached if they're past the threshold
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-max-kb  0
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (run-query)))
+
+;; check that `query-caching-max-ttl` is respected. Whenever a new query is cached the cache should evict any entries older that `query-caching-max-ttl`.
+;; Set max-ttl to one second, run query `:abc`, then wait two seconds, and run `:def`. This should trigger the cache flush for entries past `:max-ttl`;
+;; and the cached entry for `:abc` should be deleted. Running `:abc` a subsequent time should not return cached results
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-max-ttl 1
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (Thread/sleep 2000)
+    (run-query, :query :def)
+    (run-query)))
+
+;; check that *ignore-cached-results* is respected when returning results...
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (binding [cache/*ignore-cached-results* true]
+      (run-query))))
+
+;; ...but if it's set those results should still be cached for next time.
+(expect
+  :cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (binding [cache/*ignore-cached-results* true]
+      (run-query))
+    (run-query)))
+
+;; if the cache takes less than the min TTL to execute, it shouldn't be cached
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 60]
+    (clear-cache!)
+    (run-query)
+    (run-query)))
+
+;; ...but if it takes *longer* than the min TTL, it should be cached
+(expect
+  :cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 1]
+    (binding [*query-execution-delay-ms* 1200]
+      (clear-cache!)
+      (run-query)
+      (run-query))))
diff --git a/test/metabase/query_processor/sql_parameters_test.clj b/test/metabase/query_processor/sql_parameters_test.clj
index 1fbf45c83e4e6c00ad3494fe62c0542ea2802f8d..dda33ecf8b2442c1e0c354b4914fd7f1595774aa 100644
--- a/test/metabase/query_processor/sql_parameters_test.clj
+++ b/test/metabase/query_processor/sql_parameters_test.clj
@@ -428,9 +428,10 @@
   (generic-sql/quote-name datasets/*driver* identifier))
 
 (defn- checkins-identifier []
-  ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery we will just hackily return the correct identifier here
-  (if (= datasets/*engine* :bigquery)
-    "[test_data.checkins]"
+  ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery and Presto we will just hackily return the correct identifier here
+  (case datasets/*engine*
+    :bigquery "[test_data.checkins]"
+    :presto   "\"default\".\"checkins\""
     (let [{table-name :name, schema :schema} (db/select-one ['Table :name :schema], :id (data/id :checkins))]
       (str (when (seq schema)
              (str (quote-name schema) \.))
diff --git a/test/metabase/query_processor/util_test.clj b/test/metabase/query_processor/util_test.clj
index 160278cdc2f1852da20ac0c35c5eda8b99d69a4e..357583558a259029c48d0101d354c459d7c69542 100644
--- a/test/metabase/query_processor/util_test.clj
+++ b/test/metabase/query_processor/util_test.clj
@@ -94,3 +94,14 @@
     (qputil/query-hash {:query :abc})
     (qputil/query-hash {:query :abc, :constraints nil})
     (qputil/query-hash {:query :abc, :constraints {}})))
+
+;; make sure two different natiev queries have different hashes!
+(expect
+  false
+  (array=
+    (qputil/query-hash {:database    2
+                        :type        "native"
+                        :native      {:query "SELECT pg_sleep(15), 1 AS one"}})
+    (qputil/query-hash {:database    2
+                        :type        "native"
+                        :native      {:query "SELECT pg_sleep(15), 2 AS two"}})))
diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj
index 04989644a6e345eb4e400ba9936cf784657e10bd..3e0fb4f0391f77122d15acba55ae9645e5121b12 100644
--- a/test/metabase/query_processor_test.clj
+++ b/test/metabase/query_processor_test.clj
@@ -294,7 +294,7 @@
   {:style/indent 0}
   [results]
   (vec (or (get-in results [:data :rows])
-           (println (u/pprint-to-str 'red results))
+           (println (u/pprint-to-str 'red results)) ; DEBUG
            (throw (Exception. "Error!")))))
 
 (defn rows+column-names
diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj
index 759d5e26e57a62c485d473a0a5534ccaf22ad3fd..d6b8267866176ce7e2bc01ae7810ebcb0366b60f 100644
--- a/test/metabase/query_processor_test/aggregation_test.clj
+++ b/test/metabase/query_processor_test/aggregation_test.clj
@@ -149,8 +149,8 @@
             (ql/aggregation (ql/avg $price) (ql/count) (ql/sum $price))))))
 
 ;; make sure that multiple aggregations of the same type have the correct metadata (#4003)
-;; (TODO - this isn't tested against Mongo or BigQuery because those drivers don't currently work correctly with multiple columns with the same name)
-(datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery)
+;; (TODO - this isn't tested against Mongo, BigQuery or Presto because those drivers don't currently work correctly with multiple columns with the same name)
+(datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery :presto)
   [(aggregate-col :count)
    (assoc (aggregate-col :count)
      :display_name    "count_2"
diff --git a/test/metabase/query_processor_test/date_bucketing_test.clj b/test/metabase/query_processor_test/date_bucketing_test.clj
index eeee25db76c9fc3660e461a2f403d29bcd9cb6a5..603e8d93057fb3fdafc4cd9473181f0a7b6fbef5 100644
--- a/test/metabase/query_processor_test/date_bucketing_test.clj
+++ b/test/metabase/query_processor_test/date_bucketing_test.clj
@@ -37,7 +37,7 @@
      ["2015-06-02 08:20:00" 1]
      ["2015-06-02 11:11:00" 1]]
 
-    (contains? #{:redshift :sqlserver :bigquery :mongo :postgres :vertica :h2 :oracle} *engine*)
+    (contains? #{:redshift :sqlserver :bigquery :mongo :postgres :vertica :h2 :oracle :presto} *engine*)
     [["2015-06-01T10:31:00.000Z" 1]
      ["2015-06-01T16:06:00.000Z" 1]
      ["2015-06-01T17:23:00.000Z" 1]
@@ -246,7 +246,7 @@
     (contains? #{:sqlserver :sqlite :crate :oracle} *engine*)
     [[23 54] [24 46] [25 39] [26 61]]
 
-    (contains? #{:mongo :redshift :bigquery :postgres :vertica :h2} *engine*)
+    (contains? #{:mongo :redshift :bigquery :postgres :vertica :h2 :presto} *engine*)
     [[23 46] [24 47] [25 40] [26 60] [27 7]]
 
     :else
diff --git a/test/metabase/sync_database_test.clj b/test/metabase/sync_database_test.clj
index e3f963012af804337e025d6502f7d67f0ff1ecf7..d44d2967019187c16c3f7692855d908b6abc442e 100644
--- a/test/metabase/sync_database_test.clj
+++ b/test/metabase/sync_database_test.clj
@@ -326,21 +326,23 @@
 
 
 ;;; -------------------- Make sure that if a Field's cardinality passes `metabase.sync-database.analyze/low-cardinality-threshold` (currently 300) (#3215) --------------------
+(defn- insert-range-sql [rang]
+  (str "INSERT INTO blueberries_consumed (num) VALUES "
+       (str/join ", " (for [n rang]
+                        (str "(" n ")")))))
+
 (expect
   false
   (let [details {:db (str "mem:" (tu/random-name) ";DB_CLOSE_DELAY=10")}]
     (binding [mdb/*allow-potentailly-unsafe-connections* true]
       (tt/with-temp Database [db {:engine :h2, :details details}]
-        (let [driver       (driver/engine->driver :h2)
-              spec         (sql/connection-details->spec driver details)
-              exec!        #(doseq [statement %]
-                              (println (jdbc/execute! spec [statement])))
-              insert-range #(str "INSERT INTO blueberries_consumed (num) VALUES "
-                                 (str/join ", " (for [n %]
-                                                  (str "(" n ")"))))]
+        (let [driver (driver/engine->driver :h2)
+              spec   (sql/connection-details->spec driver details)
+              exec!  #(doseq [statement %]
+                        (jdbc/execute! spec [statement]))]
           ;; create the `blueberries_consumed` table and insert a 100 values
           (exec! ["CREATE TABLE blueberries_consumed (num INTEGER NOT NULL);"
-                  (insert-range (range 100))])
+                  (insert-range-sql (range 100))])
           (sync-database! db, :full-sync? true)
           (let [table-id (db/select-one-id Table :db_id (u/get-id db))
                 field-id (db/select-one-id Field :table_id table-id)]
@@ -348,6 +350,6 @@
             (assert (= (count (db/select-one-field :values FieldValues :field_id field-id))
                        100))
             ;; ok, now insert enough rows to push the field past the `low-cardinality-threshold` and sync again, there should be no more field values
-            (exec! [(insert-range (range 100 (+ 100 @(resolve 'metabase.sync-database.analyze/low-cardinality-threshold))))])
+            (exec! [(insert-range-sql (range 100 (+ 100 @(resolve 'metabase.sync-database.analyze/low-cardinality-threshold))))])
             (sync-database! db, :full-sync? true)
             (db/exists? FieldValues :field_id field-id)))))))
diff --git a/test/metabase/test/data/druid.clj b/test/metabase/test/data/druid.clj
index 59d063b122a1466449b2f4e63ff5fba7feb91a44..0f24351c182126adabbbb5461eb799b8bf687d45 100644
--- a/test/metabase/test/data/druid.clj
+++ b/test/metabase/test/data/druid.clj
@@ -27,38 +27,10 @@
 
 ;;; Setting Up a Server w/ Druid Test Data
 
-;; Unfortunately the process of loading test data onto an external server for CI purposes is a little involved. Before testing against Druid, you'll need to perform the following steps:
-;; For EC2 instances, make sure to expose ports `8082` & `8090` for Druid while loading data. Once data has finished loading, you only need to expose port `8082`.
-;;
-;; 1.  Setup Zookeeper
-;;     1A.  Download & extract Zookeeper from `http://zookeeper.apache.org/releases.html#download`
-;;     1B.  Create `zookeeper/conf/zoo.cfg` -- see the Getting Started Guide: `http://zookeeper.apache.org/doc/r3.4.6/zookeeperStarted.html`
-;;     1C.  `zookeeper/bin/zkServer.sh start`
-;;     1D.  `zookeeper/bin/zkServer.sh status` (to make sure it started correctly)
-;; 2.  Setup Druid
-;;     2A.  Download & extract Druid from `http://druid.io/downloads.html`
-;;     2B.  `cp druid/run_druid_server.sh druid/run_historical.sh` and bump the `-Xmx` setting to `6g` or so
-;;     2C.  `cd druid && ./run_druid_server.sh coordinator`
-;;     2D.  `cd druid && ./run_druid_server.sh broker`
-;;     2E.  `cd druid && ./run_historical.sh historical`
-;;     2E.  `cd druid && ./run_druid_server.sh overlord`
-;; 3.  Generate flattened test data file. Optionally pick a <filename>
-;;     3A.  From this namespace in the REPL, run `(generate-json-for-batch-ingestion <filename>)`
-;;     3B.  `scp` or otherwise upload this file to the box running druid (if applicable)
-;; 4.  Launch Druid Indexing Task
-;;     4A.  Run the indexing task on the remote instance.
-;;
-;;            (run-indexing-task <remote-host> :base-dir <dir-where-you-uploaded-file>, :filename <file>)
-;;            e.g.
-;;            (run-indexing-task "http://ec2-52-90-109-199.compute-1.amazonaws.com", :base-dir "/home/ec2-user", :filename "checkins.json")
-;;
-;;          The task will keep you apprised of its progress until it completes (takes 1-2 minutes)
-;;     4B.  Keep an eye on `<host>:8082/druid/v2/datasources`. (e.g. "http://ec2-52-90-109-199.compute-1.amazonaws.com:8082/druid/v2/datasources")
-;;          This endpoint will return an empty array until the broker knows about the newly ingested segments. When it returns an array with the string `"checkins"` you're ready to
-;;          run the tests.
-;;     4C.  Kill the `overlord` process once the data has finished loading.
-;; 5.  Running Tests
-;;     5A.  You can run tests like `ENGINES=druid MB_DRUID_PORT=8082 MB_DRUID_HOST=http://ec2-52-90-109-199.compute-1.amazonaws.com lein test`
+;; Unfortunately the process of loading test data onto an external server for CI purposes is a little involved.
+;; A complete step-by-step guide is available on the wiki at `https://github.com/metabase/metabase/wiki/Setting-up-Druid-for-CI-on-EC2`
+;; Refer to that page for more information.
+
 
 (def ^:private ^:const default-filename "Default filename for batched ingestion data file."
   "checkins.json")
@@ -126,7 +98,7 @@
 
 (def ^:private ^:const indexer-timeout-seconds
   "Maximum number of seconds we should wait for the indexing task to finish before deciding it's failed."
-  180)
+  300) ; five minutes
 
 (resolve-private-vars metabase.driver.druid GET POST)
 
@@ -139,7 +111,7 @@
     (println "STATUS URL: " (str indexer-endpoint "/" task "/status"))
     (loop [remaining-seconds indexer-timeout-seconds]
       (let [status (get-in (GET status-url) [:status :status])]
-        (println (format "%s (%d seconds elapsed)" status (- indexer-timeout-seconds remaining-seconds)))
+        (printf "%s (%d seconds elapsed)\n" status (- indexer-timeout-seconds remaining-seconds))
         (when (not= status "SUCCESS")
           (when (<= remaining-seconds 0)
             (throw (Exception. (str "Failed to finish indexing druid data after " indexer-timeout-seconds " seconds!"))))
diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj
index 5a93ba82eb14fe52b3ceb367b61a30b36f82f0cb..49a3b4fe11ae5f064e3f9f7ccb691e58422c2611 100644
--- a/test/metabase/test/data/generic_sql.clj
+++ b/test/metabase/test/data/generic_sql.clj
@@ -13,7 +13,8 @@
                                 [interface :as i])
             [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx])
-  (:import clojure.lang.Keyword
+  (:import java.sql.SQLException
+           clojure.lang.Keyword
            (metabase.test.data.interface DatabaseDefinition
                                          FieldDefinition
                                          TableDefinition)))
@@ -218,7 +219,7 @@
                                           :quoting             (sql/quote-style driver)
                                           :allow-dashed-names? true)))]
     (try (jdbc/execute! spec sql+args)
-         (catch java.sql.SQLException e
+         (catch SQLException e
            (println (u/format-color 'red "INSERT FAILED: \n%s\n" sql+args))
            (jdbc/print-sql-exception-chain e)))))
 
@@ -249,15 +250,15 @@
       (let [sql (s/replace sql #";+" ";")]
         (try
           (jdbc/execute! (database->spec driver context dbdef) [sql] {:transaction? false, :multi? true})
-          (catch java.sql.SQLException e
+          (catch SQLException e
             (println "Error executing SQL:" sql)
-            (println (format "Caught SQLException:\n%s"
-                             (with-out-str (jdbc/print-sql-exception-chain e))))
+            (printf "Caught SQLException:\n%s\n"
+                    (with-out-str (jdbc/print-sql-exception-chain e)))
             (throw e))
           (catch Throwable e
             (println "Error executing SQL:" sql)
-            (println (format "Caught Exception: %s %s\n%s" (class e) (.getMessage e)
-                             (with-out-str (.printStackTrace e))))
+            (printf "Caught Exception: %s %s\n%s\n" (class e) (.getMessage e)
+                    (with-out-str (.printStackTrace e)))
             (throw e)))))))
 
 
diff --git a/test/metabase/test/data/presto.clj b/test/metabase/test/data/presto.clj
new file mode 100644
index 0000000000000000000000000000000000000000..790907b1abf8aef6f032d5bdc710f1c8ee235c7c
--- /dev/null
+++ b/test/metabase/test/data/presto.clj
@@ -0,0 +1,107 @@
+(ns metabase.test.data.presto
+  (:require [clojure.string :as s]
+            [environ.core :refer [env]]
+            (honeysql [core :as hsql]
+                      [helpers :as h])
+            [metabase.driver.generic-sql.util.unprepare :as unprepare]
+            [metabase.test.data.interface :as i]
+            [metabase.test.util :refer [resolve-private-vars]]
+            [metabase.util :as u]
+            [metabase.util.honeysql-extensions :as hx])
+  (:import java.util.Date
+           metabase.driver.presto.PrestoDriver
+           (metabase.query_processor.interface DateTimeValue Value)))
+
+(resolve-private-vars metabase.driver.presto execute-presto-query! presto-type->base-type quote-name quote+combine-names)
+
+;;; Helpers
+
+(defn- get-env-var [env-var]
+  (or (env (keyword (format "mb-presto-%s" (name env-var))))
+      (throw (Exception. (format "In order to test Presto, you must specify the env var MB_PRESTO_%s."
+                                 (s/upper-case (s/replace (name env-var) #"-" "_")))))))
+
+
+;;; IDatasetLoader implementation
+
+(defn- database->connection-details [context {:keys [database-name]}]
+  (merge {:host (get-env-var :host)
+          :port (get-env-var :port)
+          :user "metabase"
+          :ssl  false}
+         (when (= context :db)
+           {:catalog database-name})))
+
+(defn- qualify-name
+  ;; we have to use the default schema from the in-memory connectory
+  ([db-name]                       [db-name])
+  ([db-name table-name]            [db-name "default" table-name])
+  ([db-name table-name field-name] [db-name "default" table-name field-name]))
+
+(defn- qualify+quote-name [& names]
+  (apply quote+combine-names (apply qualify-name names)))
+
+(defn- field-base-type->dummy-value [field-type]
+  ;; we need a dummy value for every base-type to make a properly typed SELECT statement
+  (if (keyword? field-type)
+    (case field-type
+      :type/Boolean    "TRUE"
+      :type/Integer    "1"
+      :type/BigInteger "cast(1 AS bigint)"
+      :type/Float      "1.0"
+      :type/Decimal    "DECIMAL '1.0'"
+      :type/Text       "cast('' AS varchar(255))"
+      :type/Date       "current_timestamp" ; this should probably be a date type, but the test data begs to differ
+      :type/DateTime   "current_timestamp"
+      "from_hex('00')") ; this might not be the best default ever
+    ;; we were given a native type, map it back to a base-type and try again
+    (field-base-type->dummy-value (presto-type->base-type field-type))))
+
+(defn- create-table-sql [{:keys [database-name]} {:keys [table-name], :as tabledef}]
+  (let [field-definitions (conj (:field-definitions tabledef) {:field-name "id", :base-type  :type/Integer})
+        dummy-values      (map (comp field-base-type->dummy-value :base-type) field-definitions)
+        columns           (map :field-name field-definitions)]
+    ;; Presto won't let us use the `CREATE TABLE (...)` form, but we can still do it creatively if we select the right types out of thin air
+    (format "CREATE TABLE %s AS SELECT * FROM (VALUES (%s)) AS t (%s) WHERE 1 = 0"
+            (qualify+quote-name database-name table-name)
+            (s/join \, dummy-values)
+            (s/join \, (map quote-name columns)))))
+
+(defn- drop-table-if-exists-sql [{:keys [database-name]} {:keys [table-name]}]
+  (str "DROP TABLE IF EXISTS " (qualify+quote-name database-name table-name)))
+
+(defn- insert-sql [{:keys [database-name]} {:keys [table-name], :as tabledef} rows]
+  (let [field-definitions (conj (:field-definitions tabledef) {:field-name "id"})
+        columns           (map (comp keyword :field-name) field-definitions)
+        [query & params]  (-> (apply h/columns columns)
+                              (h/insert-into (apply hsql/qualify (qualify-name database-name table-name)))
+                              (h/values rows)
+                              (hsql/format :allow-dashed-names? true, :quoting :ansi))]
+    (if (nil? params)
+      query
+      (unprepare/unprepare (cons query params) :quote-escape "'", :iso-8601-fn :from_iso8601_timestamp))))
+
+(defn- create-db! [{:keys [table-definitions] :as dbdef}]
+  (let [details (database->connection-details :db dbdef)]
+    (doseq [tabledef table-definitions
+            :let [rows       (:rows tabledef)
+                  keyed-rows (map-indexed (fn [i row] (conj row (inc i))) rows) ; generate an ID for each row because we don't have auto increments
+                  batches    (partition 100 100 nil keyed-rows)]]               ; make 100 rows batches since we have to inline everything
+      (execute-presto-query! details (drop-table-if-exists-sql dbdef tabledef))
+      (execute-presto-query! details (create-table-sql dbdef tabledef))
+      (doseq [batch batches]
+        (execute-presto-query! details (insert-sql dbdef tabledef batch))))))
+
+
+;;; IDatasetLoader implementation
+
+(u/strict-extend PrestoDriver
+  i/IDatasetLoader
+  (merge i/IDatasetLoaderDefaultsMixin
+         {:engine                             (constantly :presto)
+          :database->connection-details       (u/drop-first-arg database->connection-details)
+          :create-db!                         (u/drop-first-arg create-db!)
+          :default-schema                     (constantly "default")
+          :format-name                        (u/drop-first-arg s/lower-case)
+          ;; FIXME Presto actually has very good timezone support
+          :has-questionable-timezone-support? (constantly true)}))
diff --git a/test/metabase/test/data/sqlserver.clj b/test/metabase/test/data/sqlserver.clj
index 858ebdac8104c515b51a290010c551413c59999b..d4bb90ec570292308bc1cf8edddfa39f86105052 100644
--- a/test/metabase/test/data/sqlserver.clj
+++ b/test/metabase/test/data/sqlserver.clj
@@ -102,7 +102,7 @@
       (with-redefs [+suffix identity]
         (doseq [db leftover-dbs]
           (u/ignore-exceptions
-            (println (format "Deleting leftover SQL Server DB '%s'..." db))
+            (printf "Deleting leftover SQL Server DB '%s'...\n" db)
             ;; Don't try to kill other connections to this DB with SET SINGLE_USER -- some other instance (eg CI) might be using it
             (jdbc/execute! connection-spec [(format "DROP DATABASE \"%s\";" db)])
             (println "[ok]")))))))
diff --git a/test/metabase/test/data/users.clj b/test/metabase/test/data/users.clj
index d08a28c4017d6e02b3bd783927be181eb1144f30..5698ebae330aae5620c9422b05d3653aaa14eb40 100644
--- a/test/metabase/test/data/users.clj
+++ b/test/metabase/test/data/users.clj
@@ -9,7 +9,8 @@
             (metabase.models [permissions-group :as perms-group]
                              [user :refer [User]])
             [metabase.util :as u]
-            [metabase.test.util :refer [random-name]]))
+            [metabase.test.util :refer [random-name]])
+  (:import clojure.lang.ExceptionInfo))
 
 ;;; ------------------------------------------------------------ User Definitions ------------------------------------------------------------
 
@@ -59,7 +60,7 @@
      (when-not (init-status/complete?)
        (when (<= max-wait-seconds 0)
          (throw (Exception. "Metabase still hasn't finished initializing.")))
-       (println (format "Metabase is not yet initialized, waiting 1 second (max wait remaining: %d seconds)..." max-wait-seconds))
+       (printf "Metabase is not yet initialized, waiting 1 second (max wait remaining: %d seconds)...\n" max-wait-seconds)
        (Thread/sleep 1000)
        (recur (dec max-wait-seconds))))))
 
@@ -71,6 +72,7 @@
   {:pre [(string? email) (string? first) (string? last) (string? password) (m/boolean? superuser) (m/boolean? active)]}
   (wait-for-initiailization)
   (or (User :email email)
+      (println "Creating test user:" email) ; DEBUG
       (db/insert! User
         :email        email
         :first_name   first
@@ -123,7 +125,7 @@
     (fn [id]
       (@m id))))
 
-(def ^:private tokens (atom {}))
+(defonce ^:private tokens (atom {}))
 
 (defn- username->token [username]
   (or (@tokens username)
@@ -134,18 +136,15 @@
 (defn- client-fn [username & args]
   (try
     (apply http/client (username->token username) args)
-    (catch Throwable e
+    (catch ExceptionInfo e
       (let [{:keys [status-code]} (ex-data e)]
         (when-not (= status-code 401)
           (throw e))
         ;; If we got a 401 unauthenticated clear the tokens cache + recur
+        (printf "Got 401 (Unauthenticated) for %s. Clearing cached auth tokens and retrying request.\n" username) ; DEBUG
         (reset! tokens {})
         (apply client-fn username args)))))
 
-;; TODO - does it make sense just to make this a non-higher-order function? Or a group of functions, e.g.
-;; (GET :rasta 200 "field/10/values")
-;; vs.
-;; ((user->client :rasta) :get 200 "field/10/values")
 (defn user->client
   "Returns a `metabase.http-client/client` partially bound with the credentials for User with USERNAME.
    In addition, it forces lazy creation of the User if needed.
@@ -160,4 +159,4 @@
   "Delete all users besides the 4 persistent test users.
    This is a HACK to work around tests that don't properly clean up after themselves; one day we should be able to remove this. (TODO)"
   []
-  (db/delete! 'User :id [:not-in (map user->id [:crowberto :lucky :rasta :trashbird])]))
+  (db/delete! User :id [:not-in (map user->id [:crowberto :lucky :rasta :trashbird])]))
diff --git a/test/metabase/timeseries_query_processor_test.clj b/test/metabase/timeseries_query_processor_test.clj
index 5d05675e8e36c968b57db2d0015acc32577c06ec..50765b41ab1df0e3fea23515c7eac14062e67a29 100644
--- a/test/metabase/timeseries_query_processor_test.clj
+++ b/test/metabase/timeseries_query_processor_test.clj
@@ -45,7 +45,7 @@
 
 (defn- data [results]
   (when-let [data (or (:data results)
-                      (println (u/pprint-to-str results)))]
+                      (println (u/pprint-to-str results)))] ; DEBUG
     (-> data
         (select-keys [:columns :rows])
         (update :rows vec))))
@@ -69,6 +69,44 @@
   (data (data/run-query checkins
           (ql/limit 2))))
 
+;;; "bare rows" query, limit, order-by timestamp desc
+(expect-with-timeseries-dbs
+  {:columns ["id"
+             "timestamp"
+             "count"
+             "user_last_login"
+             "user_name"
+             "venue_category_name"
+             "venue_latitude"
+             "venue_longitude"
+             "venue_name"
+             "venue_price"]
+   :rows [["693", "2015-12-29T08:00:00.000Z", 1, "2014-07-03T19:30:00.000Z", "Frans Hevel", "Mexican", "34.0489", "-118.238", "Señor Fish",       "2"]
+          ["570", "2015-12-26T08:00:00.000Z", 1, "2014-07-03T01:30:00.000Z", "Kfir Caj",    "Chinese", "37.7949", "-122.406", "Empress of China", "3"]]}
+  (data (data/run-query checkins
+          (ql/order-by (ql/desc $timestamp))
+          (ql/limit 2))))
+
+;;; "bare rows" query, limit, order-by timestamp asc
+(expect-with-timeseries-dbs
+  {:columns ["id"
+             "timestamp"
+             "count"
+             "user_last_login"
+             "user_name"
+             "venue_category_name"
+             "venue_latitude"
+             "venue_longitude"
+             "venue_name"
+             "venue_price"]
+   :rows [["931", "2013-01-03T08:00:00.000Z", 1, "2014-01-01T08:30:00.000Z", "Simcha Yan", "Thai", "34.094",  "-118.344", "Kinaree Thai Bistro",       "1"]
+          ["285", "2013-01-10T08:00:00.000Z", 1, "2014-07-03T01:30:00.000Z", "Kfir Caj",   "Thai", "34.1021", "-118.306", "Ruen Pair Thai Restaurant", "2"]]}
+  (data (data/run-query checkins
+          (ql/order-by (ql/asc $timestamp))
+          (ql/limit 2))))
+
+
+
 ;;; fields clause
 (expect-with-timeseries-dbs
   {:columns ["venue_name" "venue_category_name" "timestamp"],
@@ -78,6 +116,28 @@
           (ql/fields $venue_name $venue_category_name)
           (ql/limit 2))))
 
+;;; fields clause, order by timestamp asc
+(expect-with-timeseries-dbs
+  {:columns ["venue_name" "venue_category_name" "timestamp"],
+   :rows    [["Kinaree Thai Bistro"       "Thai" "2013-01-03T08:00:00.000Z"]
+             ["Ruen Pair Thai Restaurant" "Thai" "2013-01-10T08:00:00.000Z"]]}
+  (data (data/run-query checkins
+          (ql/fields $venue_name $venue_category_name)
+          (ql/order-by (ql/asc $timestamp))
+          (ql/limit 2))))
+
+;;; fields clause, order by timestamp desc
+(expect-with-timeseries-dbs
+  {:columns ["venue_name" "venue_category_name" "timestamp"],
+   :rows    [["Señor Fish"       "Mexican" "2015-12-29T08:00:00.000Z"]
+             ["Empress of China" "Chinese" "2015-12-26T08:00:00.000Z"]]}
+  (data (data/run-query checkins
+          (ql/fields $venue_name $venue_category_name)
+          (ql/order-by (ql/desc $timestamp))
+          (ql/limit 2))))
+
+
+
 ;;; count
 (expect-with-timeseries-dbs
   [1000]
diff --git a/webpack.config.js b/webpack.config.js
index 6183bf73437d7e3d01f62dc603001ac35541fb55..ee560eb282106139e925d4155bb37dc81732a63a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -9,6 +9,7 @@ var webpackPostcssTools = require('webpack-postcss-tools');
 
 var ExtractTextPlugin = require('extract-text-webpack-plugin');
 var HtmlWebpackPlugin = require('html-webpack-plugin');
+var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
 var UnusedFilesWebpackPlugin = require("unused-files-webpack-plugin").default;
 var BannerWebpackPlugin = require('banner-webpack-plugin');
 
@@ -27,18 +28,16 @@ function hasArg(arg) {
 var SRC_PATH = __dirname + '/frontend/src/metabase';
 var BUILD_PATH = __dirname + '/resources/frontend_client';
 
+// default NODE_ENV to development
+var NODE_ENV = process.env["NODE_ENV"] || "development";
 
 // Need to scan the CSS files for variable and custom media used across files
 // NOTE: this requires "webpack -w" (watch mode) to be restarted when variables change :(
-var isWatching = hasArg("-w") || hasArg("--watch");
-if (isWatching) {
-    console.warn("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries");
+var IS_WATCHING = hasArg("-w") || hasArg("--watch");
+if (IS_WATCHING) {
+    process.stderr.write("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries\n");
 }
 
-// default NODE_ENV to development
-var NODE_ENV = process.env["NODE_ENV"] || "development";
-process.stderr.write("webpack env: " + NODE_ENV + "\n");
-
 // Babel:
 var BABEL_CONFIG = {
     cacheDirectory: ".babel_cache"
@@ -159,19 +158,25 @@ var config = module.exports = {
             filename: '../../index.html',
             chunks: ["app-main", "styles"],
             template: __dirname + '/resources/frontend_client/index_template.html',
-            inject: 'head'
+            inject: 'head',
+            alwaysWriteToDisk: true,
         }),
         new HtmlWebpackPlugin({
             filename: '../../public.html',
             chunks: ["app-public", "styles"],
             template: __dirname + '/resources/frontend_client/index_template.html',
-            inject: 'head'
+            inject: 'head',
+            alwaysWriteToDisk: true,
         }),
         new HtmlWebpackPlugin({
             filename: '../../embed.html',
             chunks: ["app-embed", "styles"],
             template: __dirname + '/resources/frontend_client/index_template.html',
-            inject: 'head'
+            inject: 'head',
+            alwaysWriteToDisk: true,
+        }),
+        new HtmlWebpackHarddiskPlugin({
+            outputPath: __dirname + '/resources/frontend_client/app/dist'
         }),
         new webpack.DefinePlugin({
             'process.env': {
diff --git a/yarn.lock b/yarn.lock
index 135d29ee41aea23d194c31b2991d5313bdf9d5df..3e9c0ec3a1ace600051a9e8209b8e5bda3aec287 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3213,7 +3213,7 @@ fb-watchman@^1.8.0, fb-watchman@^1.9.0:
   dependencies:
     bser "1.0.2"
 
-fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8:
+fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8, fbjs@^0.8.9:
   version "0.8.9"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14"
   dependencies:
@@ -3723,6 +3723,12 @@ html-minifier@^3.2.3:
     relateurl "0.2.x"
     uglify-js "2.7.x"
 
+html-webpack-harddisk-plugin@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/html-webpack-harddisk-plugin/-/html-webpack-harddisk-plugin-0.1.0.tgz#432024961a21ac668fa2b5dfe24629c60b9c58d7"
+  dependencies:
+    mkdirp "^0.5.1"
+
 html-webpack-plugin@^2.14.0:
   version "2.28.0"
   resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.28.0.tgz#2e7863b57e5fd48fe263303e2ffc934c3064d009"
@@ -6530,6 +6536,18 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
+prop-types@15.5.0-alpha.0:
+  version "15.5.0-alpha.0"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.0-alpha.0.tgz#a342108678256db125eee3d1ae2f889af3531bd7"
+  dependencies:
+    fbjs "^0.8.9"
+
+prop-types@^15.5.4, prop-types@~15.5.0:
+  version "15.5.4"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.4.tgz#2ed3692716a5060f8cc020946d8238e7419d92c0"
+  dependencies:
+    fbjs "^0.8.9"
+
 proxy-addr@~1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074"
@@ -6697,13 +6715,14 @@ react-docgen@^2.12.1:
     node-dir "^0.1.10"
     recast "^0.11.5"
 
-react-dom@^15.2.1:
-  version "15.4.2"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f"
+react-dom@15.5.0:
+  version "15.5.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.0.tgz#86a8d6dcde388473815039de3840706e1f28f697"
   dependencies:
-    fbjs "^0.8.1"
+    fbjs "^0.8.9"
     loose-envify "^1.1.0"
     object-assign "^4.1.0"
+    prop-types "~15.5.0"
 
 react-draggable@^2.1.0, react-draggable@^2.2.3:
   version "2.2.3"
@@ -6858,13 +6877,14 @@ react-virtualized@^8.6.0:
     dom-helpers "^2.4.0 || ^3.0.0"
     loose-envify "^1.3.0"
 
-react@^15.2.1:
-  version "15.4.2"
-  resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"
+react@15.5.0:
+  version "15.5.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-15.5.0.tgz#1f8e4b492dcfbf77479eb4fdfc48da425002e505"
   dependencies:
-    fbjs "^0.8.4"
+    fbjs "^0.8.9"
     loose-envify "^1.1.0"
     object-assign "^4.1.0"
+    prop-types "15.5.0-alpha.0"
 
 read-cache@^1.0.0:
   version "1.0.0"