diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 4e67076baed982ff9472d3dc2f5e34c389fd138c..a8f484af147aca756f0f64c38b4d3ec0c00dda0e 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -3,10 +3,16 @@ Before filing an issue we'd appreciate it if you could take a moment to ensure
 there isn't already an open issue or pull-request.
 -----
 
+If there's an exisiting issue, please add a :+1: reaction to the description of
+the issue. One way we prioritize issues is by the number of :+1: reactions on
+their descriptions. Please DO NOT add `+1` or :+1: comments.
+
 ### Database support
-If there's an exisiting issue, please add a +1 in the comments as we
-prioritize drivers by the level of support. Otherwise, please include the name
-of the database and the version.
+
+For requests for new database drivers, please include the name
+of the database and the version. Additionally, giving us a sense of how and what
+you use it for, and other high-level details about the database, can help us get
+a better picture of what would need to done to support it.
 
 ### Feature requests and proposals
 We're excited to hear how we can make Metabase better. Please add as much detail
diff --git a/Dockerfile b/Dockerfile
index 3c4b42355429d63053a3033881f27b73426e130e..9145b7b1d4630d129a047e9204f6545f8b5d1e61 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,7 @@
+# NOTE: this Dockerfile builds Metabase from source. We recommend deploying the pre-built
+# images hosted on Docker Hub https://hub.docker.com/r/metabase/metabase/ which use the
+# Dockerfile located at ./bin/docker/Dockerfile
+
 FROM java:openjdk-7-jre-alpine
 
 ENV JAVA_HOME=/usr/lib/jvm/default-jvm
diff --git a/bin/ci b/bin/ci
index 4869c1cd4ae31e520fe065a3278870a386de2183..915b3afebe15c6fe046532a978bf2e1827fb6b79 100755
--- a/bin/ci
+++ b/bin/ci
@@ -54,7 +54,7 @@ node-5() {
 }
 node-6() {
     if is_enabled "jar" || is_enabled "e2e" || is_enabled "screenshots"; then
-        run_step ./bin/build version frontend sample-dataset uberjar
+        run_step ./bin/build version frontend-fast sample-dataset uberjar
     fi
     if is_enabled "e2e" || is_enabled "compare_screenshots"; then
         USE_SAUCE=true \
diff --git a/bin/docker/Dockerfile b/bin/docker/Dockerfile
index e8d212030399ab65eac318df3eb5ec44fbfbb00b..876915c92fba0f71c98e28e2dffbe251eefcbd63 100644
--- a/bin/docker/Dockerfile
+++ b/bin/docker/Dockerfile
@@ -9,17 +9,14 @@ RUN apk add --update bash ttf-dejavu fontconfig
 # fix broken cacerts
 RUN apk add --update java-cacerts && \
     rm -f /usr/lib/jvm/default-jvm/jre/lib/security/cacerts && \
-    ln -s /etc/ssl/certs/java/cacerts /usr/lib/jvm/default-jvm/jre/lib/security/cacerts
+    ln -s /etc/ssl/certs/java/cacerts /usr/lib/jvm/default-jvm/jre/lib/security/cacerts && \
+    rm -rf /tmp/* /var/cache/apk/*
 
 # add Metabase jar
 COPY ./metabase.jar /app/
 
 # add our run script to the image
 COPY ./run_metabase.sh /app/
-RUN chmod 755 /app/run_metabase.sh
-
-# tidy up
-RUN rm -rf /tmp/* /var/cache/apk/*
 
 # expose our default runtime port
 EXPOSE 3000
diff --git a/bin/docker/run_metabase.sh b/bin/docker/run_metabase.sh
index db15436f991bed0bd370a3a8aa5926aca4087bbf..93ef3fa837830f9486ba2db0f323f0bb0cae667e 100755
--- a/bin/docker/run_metabase.sh
+++ b/bin/docker/run_metabase.sh
@@ -25,12 +25,98 @@ if [ ! -z "$RDS_HOSTNAME" ]; then
 fi
 
 
+# Avoid running metabase (or any server) as root where possible
+# If you want to use specific user and group ids that matches an existing
+# account on the host pass them in $MGID and $MUID when starting metabase
+MGID=${MGID:-2000}
+MUID=${MUID:-2000}
+
+# create the group if it does not exist
+# TODO: edit an existing group if MGID has changed
+getent group metabase > /dev/null 2>&1
+group_exists=$?
+if [ $group_exists -ne 0 ]; then
+    addgroup -g $MGID -S metabase
+fi
+
+# create the user if it does not exist
+# TODO: edit an existing user if MGID has changed
+id -u metabase > /dev/null 2>&1
+user_exists=$?
+if [[ $user_exists -ne 0 ]]; then
+    adduser -D -u $MUID -G metabase metabase
+fi
+
+db_file=${MB_DB_FILE:-/metabase.db}
+
+# In order to run metabase as a non-root user in docker, we need to handle various
+# cases where we where previously ran as root and have an existing database that
+# consists of a bunch of files, that are owned by root, sitting in a directory that
+# may only be writable by root. It's not safe to simply change the ownership or
+# permissions of an unknown directory that may be a volume mounted on the host, so
+# we will need to detect this and make a place that is going to be safe to set
+# permissions on.
+
+# So first some preliminary checks:
+
+# 1. Does this container have an existing H2 database file?
+# 2. or an existing H2 database in it's own directory,
+# 3. or neither?
+
+
+# is there a pre-existing files only database without a metabase specific directory?
+if ls $db_file\.* > /dev/null 2>&1; then
+    db_exists=true
+else
+    db_exists=false
+fi
+# is it an old style file
+if [[ -d "$db_file" ]]; then
+    db_directory=true
+else
+    db_directory=false
+fi
+
+# If the db exits, and it's just some files in a shared directory we could do
+# serious damage to peoples home or /tmp directories if we where to set the
+# permissions on that directory to allow metabase to create db-lock and db-part
+# file there. To keep them safe we make a new directory with the same name and
+# move the db file into the new directory. If we where passed the name of a
+# directory rather than a specific file, then we are safe to set permissions on
+# that directory so there is no need to move anything.
+
+# an example file would look like /tmp/metabase.db/metabase.db.mv.db
+new_db_dir=$(dirname $db_file)/$(basename $db_file)
+
+if [[ $db_exists = "true" && ! $db_directory = "true" ]]; then
+    mkdir $new_db_dir
+    mv $db_file\.* $new_db_dir/
+fi
+
+# and for the new install case we create the directory
+if [[ $db_exists = "false" && $db_directory = "false" ]]; then
+    mkdir $new_db_dir
+fi
+
+# the case where the DB exists and is a directory, there is nothing to do
+# so nothing happens here. This will be the normal case.
+
+# next we tell metabase use the files we just moved into the directory
+# or create the files in that directory if they don't exist.
+export MB_DB_FILE=$new_db_dir/$(basename $db_file)
+
+# TODO: print big scary warning if they are configuring an ephemeral instance
+
+chown metabase:metabase $new_db_dir $new_db_dir/* 2>/dev/null  # all that fussing makes this safe
+
 # Setup Java Options
 JAVA_OPTS="${JAVA_OPTS} -Dlogfile.path=target/log -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -server"
 
 if [ ! -z "$JAVA_TIMEZONE" ]; then
-  JAVA_OPTS="${JAVA_OPTS} -Duser.timezone=${JAVA_TIMEZONE}"
+    JAVA_OPTS="${JAVA_OPTS} -Duser.timezone=${JAVA_TIMEZONE}"
 fi
 
 # Launch the application
-exec java $JAVA_OPTS -jar /app/metabase.jar
+# exec is here twice on purpose to  ensure that metabase runs as PID 1 (the init process)
+# and thus receives signals sent to the container. This allows it to shutdown cleanly on exit
+exec su metabase -s /bin/sh -c "exec java $JAVA_OPTS -jar /app/metabase.jar"
diff --git a/docs/developers-guide.md b/docs/developers-guide.md
index 89efceb63b85cdcc0e068d964cf20492411690f8..5bc17af05e03bd541462542e5975cd7b36e786b8 100644
--- a/docs/developers-guide.md
+++ b/docs/developers-guide.md
@@ -1,9 +1,9 @@
 **This guide will teach you:**
 
-> * How to compile your own copy of Metabase
-> * How to set up a development environment
-> * How to run the Metabase Server
-> * How to contribute back to the Metabase project
+*  [How to compile your own copy of Metabase](#build-metabase)
+*  [How to set up a development environment](#development-environment)
+*  [How to run the Metabase Server](#development-server-quick-start)
+*  [How to contribute back to the Metabase project](#contributing)
 
 
 # Contributing
diff --git a/docs/operations-guide/running-metabase-on-docker.md b/docs/operations-guide/running-metabase-on-docker.md
index 5ec7dd690bc588d81c58f6d3d182f03d6f116d50..0e2821f9df9bc3cb1e59d94fc8636bc5cb950270 100644
--- a/docs/operations-guide/running-metabase-on-docker.md
+++ b/docs/operations-guide/running-metabase-on-docker.md
@@ -4,7 +4,7 @@ Metabase provides an official Docker image via Dockerhub that can be used for de
 
 ### Launching Metabase on a new container
 
-Here's a quick one-liner to get you off the ground:
+Here's a quick one-liner to get you off the ground (please note, we recommend further configuration for production deployments below):
 
     docker run -d -p 3000:3000 --name metabase metabase/metabase
 
@@ -21,10 +21,36 @@ In its default configuration Metabase uses the local filesystem to run an H2 emb
 
 To persist your data outside of the container and make it available for use between container launches we can mount a local file path inside our container.
 
-    docker run -d -p 3000:3000 -v /tmp:/tmp -e "MB_DB_FILE=/tmp/metabase.db" --name metabase metabase/metabase
+    docker run -d -p 3000:3000 -v ~/metabase-data:/metabase-data -e "MB_DB_FILE=/metabase-data/metabase.db" --name metabase metabase/metabase
 
 Now when you launch your container we are telling Metabase to use the database file at `/tmp/metabase.db` instead of its default location and we are mounting that folder from our local filesystem into the container.
 
+### Getting your config back if you stopped your container
+
+If you have previously run and configured your Metabase using the local Database and then stopped the container, your data will still be there unless you deleted the container with the `docker rm` command. To recover your previous configuration:
+
+1. Find the stopped container using the `docker ps -a` command.
+   It will look something like this:
+
+```
+    docker ps -a | grep metabase
+    ca072cd44a49        metabase/metabase        "/app/run_metabase.sh"   About an hour ago   Up About an hour          0.0.0.0:3000->3000/tcp   metabase
+    02e4dff057d2        262aa3d0f714             "/app/run_metabase.sh"   23 hours ago        Exited (0) 23 hours ago                            pedantic_hypatia
+    0d2170d4aa4a        262aa3d0f714             "/app/run_metabase.sh"   23 hours ago        Exited (0) 23 hours ago                            stoic_lumiere
+```
+   Once you have identified the stopped container with your configuration in it, save the container ID from the left most column for the next step.
+2. Use `docker commit` to create a new custom docker image from the stopped container containing your configuration.
+
+```
+     docker commit ca072cd44a49 mycompany/metabase-custom
+     sha256:9ff56186de4dd0b9bb2a37c977c3a4c9358647cde60a16f11f4c05bded1fe77a
+```
+3. Run your new image using `docker run` to get up and running again.
+```
+     docker run -d -p 3000:3000 --name metabase mycompany/metabase-custom
+     430bb02a37bb2471176e54ca323d0940c4e0ee210c3ab04262cb6576fe4ded6d
+```
+Hopefully you have your previously configured Metabase Installation back. If it's not the one you expected try a different stopped container and do these steps again.
 
 ### Using Postgres as the Metabase application database
 
@@ -43,6 +69,7 @@ In this scenario all you need to do is make sure you launch Metabase with the co
 
 Keep in mind that Metabase will be connecting from within your docker container, so make sure that either you're using a fully qualified hostname or that you've set a proper entry in your container's `/etc/hosts file`.
 
+See instructions for [migrating from H2 to MySQL or Postgres](./start.md#migrating-from-using-the-h2-database-to-mysql-or-postgres).
 
 ### Setting the Java Timezone
 
@@ -57,4 +84,31 @@ It's best to set your Java timezone to match the timezone you'd like all your re
 
 While running Metabase on docker you can use any of the custom settings from [Customizing the Metabase Jetty Webserver](./start.md#customizing-the-metabase-jetty-webserver) by setting environment variables on your docker run command.
 
+In addition to the standard custom settings there are two docker specific environment variables `MUID` and `MGID` which are used to set the user and group IDs used by metabase when running in a docker container. These settings make it possible to match file permissions when files, such as the application database, are shared between the host and the container.
+
+Here's how to use a database file, owned by your account, that is stored in your home directory:
+
+    docker run -d -v ~/my-metabase-db:/metabase.db --name metabase -e MB_DB_FILE=/metabase.db -e MUID=$UID -e MGID=$GID -p 3000:3000 metabase/metabase
+
 Now that you’ve installed Metabase, it’s time to [set it up and connect it to your database](../setting-up-metabase.md).
+
+
+### Copying the application database
+
+If you forgot to configure to the application database, it will be located at `/metabase.db/metabase.db.mv.db` in the container. You can copy this whole directory out of the container using the following command (replacing `CONTAINER_ID` with the actual container ID or name, `metabase` if you named the container):
+
+    docker cp CONTAINER_ID:/metabase.db ./
+
+The DB contents will be left in a directory named metabase.db.
+Note that some older versions of metabase stored their db in a different default location.
+
+    docker cp CONTAINER_ID:/metabase.db.mv.db metabase.db.mv.db
+    
+### Fixing OutOfMemoryErrors in some hosted environments
+
+On some hosts Metabase can fail to start with an error message like:
+
+    java.lang.OutOfMemoryError: Java heap space
+    
+If that happens, you'll need to set a JVM option to manually configure the maximum amount of memory the JVM uses for the heap. Refer
+to [these instructions](./start.md#metabase-fails-to-start-due-to-heap-space-outofmemoryerrors) for details on how to do that.
diff --git a/docs/operations-guide/start.md b/docs/operations-guide/start.md
index ab59cd165887477e196a5525b880e944bfdedad3..fbbf600b1ec094d4f09f3d06bd2e31fd40d8bcb5 100644
--- a/docs/operations-guide/start.md
+++ b/docs/operations-guide/start.md
@@ -82,7 +82,7 @@ When this happens, go to a terminal where Metabase is installed and run:
 
 in the command line to manually clear the locks. Then restart your Metabase instance.
 
-### Metabase fails to start due to OutOfMemoryErrors
+### Metabase fails to start due to PermGen OutOfMemoryErrors
 
 On Java 7, Metabase may fail to launch with a message like
 
@@ -96,8 +96,30 @@ If this happens, setting a few JVM options should fix your issue:
 
     java -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=256m -jar target/uberjar/metabase.jar
 
+You can also pass JVM arguments by setting the environment variable `JAVA_TOOL_OPTIONS`, e.g.
+
+    JAVA_TOOL_OPTIONS='-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=256m'
+
 Alternatively, you can upgrade to Java 8 instead, which will fix the issue as well.
 
+### Metabase fails to start due to Heap Space OutOfMemoryErrors
+
+Normally, the JVM can figure out how much RAM is available on the system and automatically set a sensible upper bound for heap memory usage. On certain shared hosting
+environments, however, this doesn't always work perfectly. If Metabase fails to start with an error message like
+
+    java.lang.OutOfMemoryError: Java heap space
+
+You'll just need to set a JVM option to let it know explicitly how much memory it should use for the heap space:
+
+    java -Xmx2g -jar metabase.jar
+
+Adjust this number as appropriate for your shared hosting instance. Make sure to set the number lower than the total amount of RAM available on your instance, because Metabase isn't the only process that'll be running. Generally, leaving 1-2 GB of RAM for these other processes should be enough; for example, you might set `-Xmx` to `1g` for an instance with 2 GB of RAM, `2g` for one with 4 GB of RAM, `6g` for an instance with 8 GB of RAM, and so forth. You may need to experment with these settings a bit to find the right number.
+
+As above, you can use the environment variable `JAVA_TOOL_OPTIONS` to set JVM args instead of passing them directly to `java`. This is useful when running the Docker image,
+for example.
+
+    docker run -d -p 3000:3000 -e "JAVA_TOOL_OPTIONS=-Xmx2g" metabase/metabase
+
 ### 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.
diff --git a/frontend/src/metabase/reducers-admin.js b/frontend/src/metabase/admin/admin.js
similarity index 67%
rename from frontend/src/metabase/reducers-admin.js
rename to frontend/src/metabase/admin/admin.js
index 5ee2bfab4bccf8b62087d475e3c959784e4ff6fa..3fafa57f7edcf894693e18488ea11c142ccd72e1 100644
--- a/frontend/src/metabase/reducers-admin.js
+++ b/frontend/src/metabase/admin/admin.js
@@ -6,10 +6,14 @@ import people from "metabase/admin/people/people";
 import databases from "metabase/admin/databases/database";
 import datamodel from "metabase/admin/datamodel/datamodel";
 import permissions from "metabase/admin/permissions/permissions";
+import settings from "metabase/admin/settings/settings";
 
-export default {
+import { combineReducers } from "metabase/lib/redux";
+
+export default combineReducers({
     databases,
-    datamodel: datamodel,
+    datamodel,
     people,
     permissions,
-};
+    settings
+})
diff --git a/frontend/src/metabase/admin/databases/selectors.js b/frontend/src/metabase/admin/databases/selectors.js
index 79a714068df40c20b5da3e0579fafdd89841ca46..e9371fb171f2463bf258134b5f7c2272a5f26e5d 100644
--- a/frontend/src/metabase/admin/databases/selectors.js
+++ b/frontend/src/metabase/admin/databases/selectors.js
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
 
 
 // Database List
-export const databases         = state => state.databases.databases;
+export const databases         = state => state.admin.databases.databases;
 
 export const getDatabasesSorted = createSelector(
     [databases],
@@ -19,5 +19,5 @@ export const hasSampleDataset = createSelector(
 
 
 // Database Edit
-export const getEditingDatabase   = state => state.databases.editingDatabase;
-export const getFormState         = state => state.databases.formState;
+export const getEditingDatabase   = state => state.admin.databases.editingDatabase;
+export const getFormState         = state => state.admin.databases.formState;
diff --git a/frontend/src/metabase/admin/datamodel/datamodel.js b/frontend/src/metabase/admin/datamodel/datamodel.js
index dfed708e8e75b371139a1e46da5a759bb337dfe4..0dafaacccd0e8920d49e9681a8da04c7d02f370c 100644
--- a/frontend/src/metabase/admin/datamodel/datamodel.js
+++ b/frontend/src/metabase/admin/datamodel/datamodel.js
@@ -9,12 +9,15 @@ import { isFK } from "metabase/lib/types";
 
 import { MetabaseApi, SegmentApi, MetricApi, RevisionsApi } from "metabase/services";
 
+import { getEditingDatabase } from "./selectors";
+
 function loadDatabaseMetadata(databaseId) {
     return MetabaseApi.db_metadata({ 'dbId': databaseId });
 }
 
 // initializeMetadata
-export const initializeMetadata = createThunkAction("INITIALIZE_METADATA", function(databaseId, tableId) {
+export const INITIALIZE_METADATA = "metabase/admin/datamodel/INITIALIZE_METADATA";
+export const initializeMetadata = createThunkAction(INITIALIZE_METADATA, function(databaseId, tableId) {
     return async function(dispatch, getState) {
         let databases, database;
         try {
@@ -43,7 +46,8 @@ export const initializeMetadata = createThunkAction("INITIALIZE_METADATA", funct
 });
 
 // fetchDatabaseIdfields
-export const fetchDatabaseIdfields = createThunkAction("FETCH_IDFIELDS", function(databaseId) {
+export const FETCH_IDFIELDS = "metabase/admin/datamodel/FETCH_IDFIELDS";
+export const fetchDatabaseIdfields = createThunkAction(FETCH_IDFIELDS, function(databaseId) {
     return async function(dispatch, getState) {
         try {
             let idfields = await MetabaseApi.db_idfields({ 'dbId': databaseId });
@@ -58,7 +62,8 @@ export const fetchDatabaseIdfields = createThunkAction("FETCH_IDFIELDS", functio
 });
 
 // selectDatabase
-export const selectDatabase = createThunkAction("SELECT_DATABASE", function(db) {
+export const SELECT_DATABASE = "metabase/admin/datamodel/SELECT_DATABASE";
+export const selectDatabase = createThunkAction(SELECT_DATABASE, function(db) {
     return async function(dispatch, getState) {
         try {
             let database = await loadDatabaseMetadata(db.id);
@@ -76,7 +81,8 @@ export const selectDatabase = createThunkAction("SELECT_DATABASE", function(db)
 });
 
 // selectTable
-export const selectTable = createThunkAction("SELECT_TABLE", function(table) {
+export const SELECT_TABLE = "metabase/admin/datamodel/SELECT_TABLE";
+export const selectTable = createThunkAction(SELECT_TABLE, function(table) {
     return function(dispatch, getState) {
         // we also want to update our url to match our new state
         dispatch(push('/admin/datamodel/database/'+table.db_id+'/table/'+table.id));
@@ -86,7 +92,8 @@ export const selectTable = createThunkAction("SELECT_TABLE", function(table) {
 });
 
 // updateTable
-export const updateTable = createThunkAction("UPDATE_TABLE", function(table) {
+export const UPDATE_TABLE = "metabase/admin/datamodel/UPDATE_TABLE";
+export const updateTable = createThunkAction(UPDATE_TABLE, function(table) {
     return async function(dispatch, getState) {
         try {
             // make sure we don't send all the computed metadata
@@ -109,9 +116,10 @@ export const updateTable = createThunkAction("UPDATE_TABLE", function(table) {
 });
 
 // updateField
-export const updateField = createThunkAction("UPDATE_FIELD", function(field) {
+export const UPDATE_FIELD = "metabase/admin/datamodel/UPDATE_FIELD";
+export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
     return async function(dispatch, getState) {
-        const { datamodel: { editingDatabase } } = getState();
+        const editingDatabase = getEditingDatabase(getState());
 
         try {
             // make sure we don't send all the computed metadata
@@ -138,7 +146,8 @@ export const updateField = createThunkAction("UPDATE_FIELD", function(field) {
 });
 
 // updateFieldSpecialType
-export const updateFieldSpecialType = createThunkAction("UPDATE_FIELD_SPECIAL_TYPE", function(field) {
+export const UPDATE_FIELD_SPECIAL_TYPE = "metabase/admin/datamodel/UPDATE_FIELD_SPECIAL_TYPE";
+export const updateFieldSpecialType = createThunkAction(UPDATE_FIELD_SPECIAL_TYPE, function(field) {
     return function(dispatch, getState) {
 
         // If we are changing the field from a FK to something else, we should delete any FKs present
@@ -157,7 +166,8 @@ export const updateFieldSpecialType = createThunkAction("UPDATE_FIELD_SPECIAL_TY
 });
 
 // updateFieldTarget
-export const updateFieldTarget = createThunkAction("UPDATE_FIELD_TARGET", function(field) {
+export const UPDATE_FIELD_TARGET = "metabase/admin/datamodel/UPDATE_FIELD_TARGET";
+export const updateFieldTarget = createThunkAction(UPDATE_FIELD_TARGET, function(field) {
     return function(dispatch, getState) {
         // This function notes a change in the target of the target of a foreign key
         dispatch(updateField(field));
@@ -167,9 +177,10 @@ export const updateFieldTarget = createThunkAction("UPDATE_FIELD_TARGET", functi
 });
 
 // retireSegment
-export const onRetireSegment = createThunkAction("RETIRE_SEGMENT", function(segment) {
+export const RETIRE_SEGMENT = "metabase/admin/datamodel/RETIRE_SEGMENT";
+export const onRetireSegment = createThunkAction(RETIRE_SEGMENT, function(segment) {
     return async function(dispatch, getState) {
-        const { datamodel: { editingDatabase } } = getState();
+        const editingDatabase = getEditingDatabase(getState());
 
         await SegmentApi.delete(segment);
         MetabaseAnalytics.trackEvent("Data Model", "Retire Segment");
@@ -179,9 +190,10 @@ export const onRetireSegment = createThunkAction("RETIRE_SEGMENT", function(segm
 });
 
 // retireMetric
-export const onRetireMetric = createThunkAction("RETIRE_METRIC", function(metric) {
+export const RETIRE_METRIC = "metabase/admin/datamodel/RETIRE_METRIC";
+export const onRetireMetric = createThunkAction(RETIRE_METRIC, function(metric) {
     return async function(dispatch, getState) {
-        const { datamodel: { editingDatabase } } = getState();
+        const editingDatabase = getEditingDatabase(getState());
 
         await MetricApi.delete(metric);
         MetabaseAnalytics.trackEvent("Data Model", "Retire Metric");
@@ -193,10 +205,10 @@ export const onRetireMetric = createThunkAction("RETIRE_METRIC", function(metric
 
 // SEGMENTS
 
-export const GET_SEGMENT = "GET_SEGMENT";
-export const CREATE_SEGMENT = "CREATE_SEGMENT";
-export const UPDATE_SEGMENT = "UPDATE_SEGMENT";
-export const DELETE_SEGMENT = "DELETE_SEGMENT";
+export const GET_SEGMENT = "metabase/admin/datamodel/GET_SEGMENT";
+export const CREATE_SEGMENT = "metabase/admin/datamodel/CREATE_SEGMENT";
+export const UPDATE_SEGMENT = "metabase/admin/datamodel/UPDATE_SEGMENT";
+export const DELETE_SEGMENT = "metabase/admin/datamodel/DELETE_SEGMENT";
 
 export const getSegment    = createAction(GET_SEGMENT, SegmentApi.get);
 export const createSegment = createAction(CREATE_SEGMENT, SegmentApi.create);
@@ -205,10 +217,10 @@ export const deleteSegment = createAction(DELETE_SEGMENT, SegmentApi.delete);
 
 // METRICS
 
-export const GET_METRIC = "GET_METRIC";
-export const CREATE_METRIC = "CREATE_METRIC";
-export const UPDATE_METRIC = "UPDATE_METRIC";
-export const DELETE_METRIC = "DELETE_METRIC";
+export const GET_METRIC = "metabase/admin/datamodel/GET_METRIC";
+export const CREATE_METRIC = "metabase/admin/datamodel/CREATE_METRIC";
+export const UPDATE_METRIC = "metabase/admin/datamodel/UPDATE_METRIC";
+export const DELETE_METRIC = "metabase/admin/datamodel/DELETE_METRIC";
 
 export const getMetric    = createAction(GET_METRIC, MetricApi.get);
 export const createMetric = createAction(CREATE_METRIC, MetricApi.create);
@@ -217,8 +229,8 @@ export const deleteMetric = createAction(DELETE_METRIC, MetricApi.delete);
 
 // SEGMENT DETAIL
 
-export const LOAD_TABLE_METADATA = "LOAD_TABLE_METADATA";
-export const UPDATE_PREVIEW_SUMMARY = "UPDATE_PREVIEW_SUMMARY";
+export const LOAD_TABLE_METADATA = "metabase/admin/datamodel/LOAD_TABLE_METADATA";
+export const UPDATE_PREVIEW_SUMMARY = "metabase/admin/datamodel/UPDATE_PREVIEW_SUMMARY";
 
 export const loadTableMetadata = createAction(LOAD_TABLE_METADATA, loadTableAndForeignKeys);
 export const updatePreviewSummary = createAction(UPDATE_PREVIEW_SUMMARY, async (query) => {
@@ -228,7 +240,7 @@ export const updatePreviewSummary = createAction(UPDATE_PREVIEW_SUMMARY, async (
 
 // REVISION HISTORY
 
-export const FETCH_REVISIONS = "FETCH_REVISIONS";
+export const FETCH_REVISIONS = "metabase/admin/datamodel/FETCH_REVISIONS";
 
 export const fetchRevisions = createThunkAction(FETCH_REVISIONS, ({ entity, id }) =>
     async (dispatch, getState) => {
@@ -250,23 +262,23 @@ export const fetchRevisions = createThunkAction(FETCH_REVISIONS, ({ entity, id }
 // reducers
 
 const databases = handleActions({
-    ["INITIALIZE_METADATA"]: { next: (state, { payload }) => payload.databases }
+    [INITIALIZE_METADATA]: { next: (state, { payload }) => payload.databases }
 }, []);
 
 const idfields = handleActions({
-    ["FETCH_IDFIELDS"]: { next: (state, { payload }) => payload ? payload : state }
+    [FETCH_IDFIELDS]: { next: (state, { payload }) => payload ? payload : state }
 }, []);
 
 const editingDatabase = handleActions({
-    ["INITIALIZE_METADATA"]: { next: (state, { payload }) => payload.database },
-    ["SELECT_DATABASE"]: { next: (state, { payload }) => payload ? payload : state },
-    ["RETIRE_SEGMENT"]: { next: (state, { payload }) => payload },
-    ["RETIRE_METRIC"]: { next: (state, { payload }) => payload }
+    [INITIALIZE_METADATA]: { next: (state, { payload }) => payload.database },
+    [SELECT_DATABASE]: { next: (state, { payload }) => payload ? payload : state },
+    [RETIRE_SEGMENT]: { next: (state, { payload }) => payload },
+    [RETIRE_METRIC]: { next: (state, { payload }) => payload }
 }, null);
 
 const editingTable = handleActions({
-    ["INITIALIZE_METADATA"]: { next: (state, { payload }) => payload.tableId || null },
-    ["SELECT_TABLE"]: { next: (state, { payload }) => payload }
+    [INITIALIZE_METADATA]: { next: (state, { payload }) => payload.tableId || null },
+    [SELECT_TABLE]: { next: (state, { payload }) => payload }
 }, null);
 
 const segments = handleActions({
diff --git a/frontend/src/metabase/admin/datamodel/selectors.js b/frontend/src/metabase/admin/datamodel/selectors.js
index 2d957762ac9adfe9ce8289576d1bbf41f2d72800..5e782bc986437b37540130591b33abfc89363087 100644
--- a/frontend/src/metabase/admin/datamodel/selectors.js
+++ b/frontend/src/metabase/admin/datamodel/selectors.js
@@ -3,12 +3,12 @@ import { createSelector } from 'reselect';
 import { computeMetadataStrength } from "metabase/lib/schema_metadata";
 
 
-const segmentsSelector         = (state, props) => state.datamodel.segments;
-const metricsSelector          = (state, props) => state.datamodel.metrics;
+const segmentsSelector         = (state, props) => state.admin.datamodel.segments;
+const metricsSelector          = (state, props) => state.admin.datamodel.metrics;
 
-const tableMetadataSelector    = (state, props) => state.datamodel.tableMetadata;
-const previewSummarySelector   = (state, props) => state.datamodel.previewSummary;
-const revisionObjectSelector   = (state, props) => state.datamodel.revisionObject;
+const tableMetadataSelector    = (state, props) => state.admin.datamodel.tableMetadata;
+const previewSummarySelector   = (state, props) => state.admin.datamodel.previewSummary;
+const revisionObjectSelector   = (state, props) => state.admin.datamodel.revisionObject;
 
 const idSelector               = (state, props) => props.params.id == null ? null : parseInt(props.params.id);
 const tableIdSelector          = (state, props) => props.location.query.table == null ? null : parseInt(props.location.query.table);
@@ -73,13 +73,14 @@ export const revisionHistorySelectors = createSelector(
 );
 
 
-export const getDatabases             = (state, props) => state.datamodel.databases;
-export const getDatabaseIdfields      = (state, props) => state.datamodel.idfields;
-export const getEditingTable          = (state, props) => state.datamodel.editingTable;
+export const getDatabases             = (state, props) => state.admin.datamodel.databases;
+export const getDatabaseIdfields      = (state, props) => state.admin.datamodel.idfields;
+export const getEditingTable          = (state, props) => state.admin.datamodel.editingTable;
+export const getEditingDatabase       = (state, props) => state.admin.datamodel.editingDatabase;
 
 
 export const getEditingDatabaseWithTableMetadataStrengths = createSelector(
-    state => state.datamodel.editingDatabase,
+    state => state.admin.datamodel.editingDatabase,
     (database) => {
         if (!database || !database.tables) {
             return null;
diff --git a/frontend/src/metabase/admin/people/selectors.js b/frontend/src/metabase/admin/people/selectors.js
index 4ee7eabd4483201d1a818e905a63ef06476d2fec..05398a59dbee38f1ffbd211bbd450abac0c96df6 100644
--- a/frontend/src/metabase/admin/people/selectors.js
+++ b/frontend/src/metabase/admin/people/selectors.js
@@ -2,14 +2,14 @@
 import { createSelector } from 'reselect';
 import _ from "underscore";
 
-export const getGroups = (state) => state.people.groups;
-export const getGroup  = (state) => state.people.group;
-export const getModal  = (state) => state.people.modal;
-export const getMemberships = (state) => state.people.memberships;
+export const getGroups = (state) => state.admin.people.groups;
+export const getGroup  = (state) => state.admin.people.group;
+export const getModal  = (state) => state.admin.people.modal;
+export const getMemberships = (state) => state.admin.people.memberships;
 
 export const getUsers  = createSelector(
-    (state) => state.people.users,
-    (state) => state.people.memberships,
+    (state) => state.admin.people.users,
+    (state) => state.admin.people.memberships,
     (users, memberships) =>
         users && _.mapObject(users, user => ({
             ...user,
diff --git a/frontend/src/metabase/admin/permissions/permissions.js b/frontend/src/metabase/admin/permissions/permissions.js
index 0973b926ca316cd9617c62bf029dfeeb9c29e524..6fdc64cfe41699c360e012c638e3764502feb375 100644
--- a/frontend/src/metabase/admin/permissions/permissions.js
+++ b/frontend/src/metabase/admin/permissions/permissions.js
@@ -34,7 +34,7 @@ export const loadGroups = createAction(LOAD_GROUPS, () => PermissionsApi.groups(
 const LOAD_PERMISSIONS = "metabase/admin/permissions/LOAD_PERMISSIONS";
 export const loadPermissions = createThunkAction(LOAD_PERMISSIONS, () =>
     async (dispatch, getState) => {
-        const { load } = getState().permissions;
+        const { load } = getState().admin.permissions;
         return load();
     }
 );
@@ -56,7 +56,7 @@ const SAVE_PERMISSIONS = "metabase/admin/permissions/SAVE_PERMISSIONS";
 export const savePermissions = createThunkAction(SAVE_PERMISSIONS, () =>
     async (dispatch, getState) => {
         MetabaseAnalytics.trackEvent("Permissions", "save");
-        const { permissions, revision, save } = getState().permissions;
+        const { permissions, revision, save } = getState().admin.permissions;
         let result = await save({
             revision: revision,
             groups: permissions
diff --git a/frontend/src/metabase/admin/permissions/selectors.js b/frontend/src/metabase/admin/permissions/selectors.js
index 85510bee59995fd0d021deae11d1dfd3e9201eef..efaad73d99df7353ee2fcb41a4ec6f042cffff39 100644
--- a/frontend/src/metabase/admin/permissions/selectors.js
+++ b/frontend/src/metabase/admin/permissions/selectors.js
@@ -27,14 +27,14 @@ import {
     diffPermissions,
 } from "metabase/lib/permissions";
 
-const getPermissions = (state) => state.permissions.permissions;
-const getOriginalPermissions = (state) => state.permissions.originalPermissions;
+const getPermissions = (state) => state.admin.permissions.permissions;
+const getOriginalPermissions = (state) => state.admin.permissions.originalPermissions;
 
 const getDatabaseId = (state, props) => props.params.databaseId ? parseInt(props.params.databaseId) : null
 const getSchemaName = (state, props) => props.params.schemaName
 
 const getMetadata = createSelector(
-    [(state) => state.permissions.databases],
+    [(state) => state.admin.permissions.databases],
     (databases) => databases && new Metadata(databases)
 );
 
@@ -53,7 +53,7 @@ function getTooltipForGroup(group) {
 }
 
 export const getGroups = createSelector(
-    (state) => state.permissions.groups,
+    (state) => state.admin.permissions.groups,
     (groups) => {
         let orderedGroups = groups ? [...groups] : [];
         for (let groupFilter of SPECIAL_GROUP_FILTERS) {
@@ -75,7 +75,7 @@ export const getIsDirty = createSelector(
         JSON.stringify(permissions) !== JSON.stringify(originalPermissions)
 )
 
-export const getSaveError = (state) => state.permissions.saveError;
+export const getSaveError = (state) => state.admin.permissions.saveError;
 
 
 // these are all the permission levels ordered by level of access
@@ -418,7 +418,7 @@ export const getDatabasesPermissionsGrid = createSelector(
     }
 );
 
-const getCollections = (state) => state.permissions.collections;
+const getCollections = (state) => state.admin.permissions.collections;
 const getCollectionPermission = (permissions, groupId, { collectionId }) =>
     getIn(permissions, [groupId, collectionId])
 
diff --git a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
index de3639693cb4af2697575517e404cb079693df46..a74be885760e404ad3e38bd688c978246895d7ae 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
@@ -31,12 +31,19 @@ export default class SettingsEmailForm extends Component {
 
     componentWillMount() {
         // this gives us an opportunity to load up our formData with any existing values for elements
+        this.updateFormData(this.props);
+    }
+
+    componentWillReceiveProps(nextProps) {
+        this.updateFormData(nextProps);
+    }
+
+    updateFormData(props) {
         let formData = {};
-        this.props.elements.forEach(function(element) {
+        for (const element of props.elements) {
             formData[element.key] = element.value;
-        });
-
-        this.setState({formData});
+        }
+        this.setState({ formData });
     }
 
     componentDidMount() {
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
index 8e5498b759b80f4d52b3b19588a7084c33d1ccd3..998b5a826fcb82b286a77f46a6fc00e46c739f4c 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
@@ -45,6 +45,9 @@ export default class SettingsSetting extends Component {
                 { errorMessage &&
                     <div className="text-error text-bold pt1">{errorMessage}</div>
                 }
+                { setting.warning &&
+                    <div className="text-gold text-bold pt1">{setting.warning}</div>
+                }
             </li>
         );
     }
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index ae501e697ee409d3393db53d75673754fa40a973..1303d13df831c3ab32ca578b33f4285763d7cceb 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -95,7 +95,7 @@ const SECTIONS = [
                 display_name: "SMTP Security",
                 description: null,
                 type: "radio",
-                options: { none: "None", ssl: "SSL", tls: "TLS" },
+                options: { none: "None", ssl: "SSL", tls: "TLS", starttls: "STARTTLS" },
                 defaultValue: 'none'
             },
             {
@@ -270,7 +270,15 @@ for (const section of SECTIONS) {
     section.slug = slugify(section.name);
 }
 
-export const getSettings = state => state.settings.settings;
+export const getSettings = createSelector(
+    state => state.settings.settings,
+    state => state.admin.settings.warnings,
+    (settings, warnings) =>
+        settings.map(setting => warnings[setting.key] ?
+            { ...setting, warning: warnings[setting.key] } :
+            setting
+        )
+)
 
 export const getSettingValues = createSelector(
     getSettings,
diff --git a/frontend/src/metabase/admin/settings/settings.js b/frontend/src/metabase/admin/settings/settings.js
index fa9db3da3aeac235527426350ee8bb2fe171e1e3..cc63d4829c5fc1a0545b1fe181c2c951900b4cc2 100644
--- a/frontend/src/metabase/admin/settings/settings.js
+++ b/frontend/src/metabase/admin/settings/settings.js
@@ -1,12 +1,14 @@
 
-import { createThunkAction } from "metabase/lib/redux";
+import { createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
 
 import { SettingsApi, EmailApi, SlackApi } from "metabase/services";
 
 import { refreshSiteSettings } from "metabase/redux/settings";
 
-// initializeSettings
-export const initializeSettings = createThunkAction("INITIALIZE_SETTINGS", function() {
+// ACITON TYPES AND ACTION CREATORS
+
+export const INITIALIZE_SETTINGS = "metabase/admin/settings/INITIALIZE_SETTINGS";
+export const initializeSettings = createThunkAction(INITIALIZE_SETTINGS, function() {
     return async function(dispatch, getState) {
         try {
             await dispatch(refreshSiteSettings());
@@ -17,8 +19,8 @@ export const initializeSettings = createThunkAction("INITIALIZE_SETTINGS", funct
     };
 });
 
-// updateSetting
-export const updateSetting = createThunkAction("UPDATE_SETTING", function(setting) {
+export const UPDATE_SETTING = "metabase/admin/settings/UPDATE_SETTING";
+export const updateSetting = createThunkAction(UPDATE_SETTING, function(setting) {
     return async function(dispatch, getState) {
         try {
             await SettingsApi.put(setting);
@@ -30,12 +32,13 @@ export const updateSetting = createThunkAction("UPDATE_SETTING", function(settin
     };
 });
 
-// updateEmailSettings
-export const updateEmailSettings = createThunkAction("UPDATE_EMAIL_SETTINGS", function(settings) {
+export const UPDATE_EMAIL_SETTINGS = "metabase/admin/settings/UPDATE_EMAIL_SETTINGS";
+export const updateEmailSettings = createThunkAction(UPDATE_EMAIL_SETTINGS, function(settings) {
     return async function(dispatch, getState) {
         try {
-            await EmailApi.updateSettings(settings);
+            const result = await EmailApi.updateSettings(settings);
             await dispatch(refreshSiteSettings());
+            return result;
         } catch(error) {
             console.log("error updating email settings", settings, error);
             throw error;
@@ -43,8 +46,8 @@ export const updateEmailSettings = createThunkAction("UPDATE_EMAIL_SETTINGS", fu
     };
 });
 
-// sendTestEmail
-export const sendTestEmail = createThunkAction("SEND_TEST_EMAIL", function() {
+export const SEND_TEST_EMAIL = "metabase/admin/settings/SEND_TEST_EMAIL";
+export const sendTestEmail = createThunkAction(SEND_TEST_EMAIL, function() {
     return async function(dispatch, getState) {
         try {
             await EmailApi.sendTest();
@@ -55,8 +58,8 @@ export const sendTestEmail = createThunkAction("SEND_TEST_EMAIL", function() {
     };
 });
 
-// updateSlackSettings
-export const updateSlackSettings = createThunkAction("UPDATE_SLACK_SETTINGS", function(settings) {
+export const UPDATE_SLACK_SETTINGS = "metabase/admin/settings/UPDATE_SLACK_SETTINGS";
+export const updateSlackSettings = createThunkAction(UPDATE_SLACK_SETTINGS, function(settings) {
     return async function(dispatch, getState) {
         try {
             await SlackApi.updateSettings(settings);
@@ -68,8 +71,19 @@ export const updateSlackSettings = createThunkAction("UPDATE_SLACK_SETTINGS", fu
     };
 }, {});
 
-export const reloadSettings = createThunkAction("RELOAD_SETTINGS", function() {
+export const RELOAD_SETTINGS = "metabase/admin/settings/RELOAD_SETTINGS";
+export const reloadSettings = createThunkAction(RELOAD_SETTINGS, function() {
     return async function(dispatch, getState) {
         await dispatch(refreshSiteSettings());
     }
 });
+
+// REDUCERS
+
+export const warnings = handleActions({
+    [UPDATE_EMAIL_SETTINGS]: { next: (state, { payload }) => payload["with-corrections"] }
+}, {});
+
+export default combineReducers({
+    warnings
+});
diff --git a/frontend/src/metabase/app-main.js b/frontend/src/metabase/app-main.js
index 3d50f8bdcb63977f2e91552d1c0fa35e251ede6e..169ad090a05c8749f8c21dff82506369a2b95353 100644
--- a/frontend/src/metabase/app-main.js
+++ b/frontend/src/metabase/app-main.js
@@ -1,7 +1,47 @@
-import { init } from "./app";
 
-import { getRoutes } from "./routes.jsx";
-import reducers from './reducers-main';
+import { push } from 'react-router-redux'
 
-init(reducers, getRoutes, () => {
+import { init } from "metabase/app";
+import { getRoutes } from "metabase/routes.jsx";
+import reducers from 'metabase/reducers-main';
+
+import api from "metabase/lib/api";
+
+import { setErrorPage } from "metabase/redux/app";
+import { clearCurrentUser } from "metabase/redux/user";
+
+// we shouldn't redirect these URLs because we want to handle them differently
+const WHITELIST_FORBIDDEN_URLS = [
+    // on dashboards, we show permission errors for individual cards we don't have access to
+    /api\/card\/\d+\/query$/,
+    // metadata endpoints should not cause redirects
+    // we should gracefully handle cases where we don't have access to metadata
+    /api\/database\/\d+\/metadata$/,
+    /api\/database\/\d+\/fields/,
+    /api\/field\/\d+\/values/,
+    /api\/table\/\d+\/query_metadata$/,
+    /api\/table\/\d+\/fks$/
+];
+
+init(reducers, getRoutes, (store) => {
+    // received a 401 response
+    api.on("401", (url) => {
+        if (url.indexOf("/api/user/current") >= 0) {
+            return
+        }
+        store.dispatch(clearCurrentUser());
+        store.dispatch(push("/auth/login"));
+    });
+
+    // received a 403 response
+    api.on("403", (url) => {
+        if (url) {
+            for (const regex of WHITELIST_FORBIDDEN_URLS) {
+                if (regex.test(url)) {
+                    return;
+                }
+            }
+        }
+        store.dispatch(setErrorPage({ status: 403 }));
+    });
 })
diff --git a/frontend/src/metabase/app.js b/frontend/src/metabase/app.js
index f1538f24164e4dac3c19f2fbe16f82dd3199c7a7..5cefe8294d390108453703d02b6d256ff8d348cf 100644
--- a/frontend/src/metabase/app.js
+++ b/frontend/src/metabase/app.js
@@ -15,12 +15,10 @@ import api from "metabase/lib/api";
 import { getStore } from './store'
 
 import { refreshSiteSettings } from "metabase/redux/settings";
-import { setErrorPage } from "metabase/redux/app";
-import { clearCurrentUser } from "metabase/redux/user";
 
 import { Router, useRouterHistory } from "react-router";
 import { createHistory } from 'history'
-import { push, syncHistoryWithStore } from 'react-router-redux';
+import { syncHistoryWithStore } from 'react-router-redux';
 
 // remove trailing slash
 const BASENAME = window.MetabaseRoot.replace(/\/+$/, "");
@@ -31,18 +29,6 @@ const browserHistory = useRouterHistory(createHistory)({
     basename: BASENAME
 });
 
-// we shouldn't redirect these URLs because we want to handle them differently
-const WHITELIST_FORBIDDEN_URLS = [
-    // on dashboards, we show permission errors for individual cards we don't have access to
-    /api\/card\/\d+\/query$/,
-    // metadata endpoints should not cause redirects
-    // we should gracefully handle cases where we don't have access to metadata
-    /api\/database\/\d+\/metadata$/,
-    /api\/database\/\d+\/fields/,
-    /api\/table\/\d+\/query_metadata$/,
-    /api\/table\/\d+\/fks$/
-];
-
 function _init(reducers, getRoutes, callback) {
     const store = getStore(reducers, browserHistory);
     const routes = getRoutes(store);
@@ -70,27 +56,6 @@ function _init(reducers, getRoutes, callback) {
         window['ga-disable-' + MetabaseSettings.get('ga_code')] = MetabaseSettings.isTrackingEnabled() ? null : true;
     });
 
-    // received a 401 response
-    api.on("401", (url) => {
-        if (url.indexOf("/api/user/current") >= 0) {
-            return
-        }
-        store.dispatch(clearCurrentUser());
-        store.dispatch(push("/auth/login"));
-    });
-
-    // received a 403 response
-    api.on("403", (url) => {
-        if (url) {
-            for (const regex of WHITELIST_FORBIDDEN_URLS) {
-                if (regex.test(url)) {
-                    return;
-                }
-            }
-        }
-        store.dispatch(setErrorPage({ status: 403 }));
-    });
-
     if (callback) {
         callback(store);
     }
diff --git a/frontend/src/metabase/components/EditBar.jsx b/frontend/src/metabase/components/EditBar.jsx
index 9f18730d62548d02446ed26b5456dcd0dd36c0ff..1f537102d4239a5d357b47a2759d3a378e88be7f 100644
--- a/frontend/src/metabase/components/EditBar.jsx
+++ b/frontend/src/metabase/components/EditBar.jsx
@@ -33,7 +33,7 @@ class EditBar extends Component {
                         {subtitle}
                     </span>
                 )}
-                <span className="flex-align-right">
+                <span className="flex-align-right flex">
                     {buttons}
                 </span>
             </div>
diff --git a/frontend/src/metabase/components/EmptyState.jsx b/frontend/src/metabase/components/EmptyState.jsx
index 323cdb10afc4b0c79251838e7ca1237144d6d3b8..2aed7096b7a40aa901fdd016b58d32a74eeab458 100644
--- a/frontend/src/metabase/components/EmptyState.jsx
+++ b/frontend/src/metabase/components/EmptyState.jsx
@@ -1,35 +1,51 @@
+/* @flow */
 import React from "react";
-import PropTypes from "prop-types";
-import { Link } from "react-router";
+import {Link} from "react-router";
+import cx from "classnames";
+/*
+ * EmptyState is a component that can
+ *  1) introduce a new section of Metabase to a user and encourage the user to take an action
+ *  2) indicate an empty result after a user-triggered search/query
+ */
 
 import Icon from "metabase/components/Icon.jsx";
 
-const EmptyState = ({ title, message, icon, image, action, link }) =>
-    <div className="text-centered text-brand-light my2">
+type EmptyStateProps = {
+    message: (string | React$Element<any>),
+    title?: string,
+    icon?: string,
+    image?: string,
+    imageClassName?: string,
+    action?: string,
+    link?: string,
+    onActionClick?: () => void,
+    smallDescription?: boolean
+}
+
+const EmptyState = ({title, message, icon, image, imageClassName, action, link, onActionClick, smallDescription = false}: EmptyStateProps) =>
+    <div className="text-centered text-brand-light my2" style={smallDescription ? {} : {width: "350px"}}>
         { title &&
-            <h2 className="text-brand mb4">{title}</h2>
+        <h2 className="text-brand mb4">{title}</h2>
         }
         { icon &&
-            <Icon name={icon} size={40} />
+        <Icon name={icon} size={40}/>
         }
         { image &&
-            <img src={`${image}.png`} height="250px" alt={message} srcSet={`${image}@2x.png 2x`} />
+        <img src={`${image}.png`} width="300px" alt={message} srcSet={`${image}@2x.png 2x`}
+             className={imageClassName}/>
         }
         <div className="flex justify-center">
-            <h3 className="text-grey-2 mt4">{message}</h3>
+            <h2 className={cx("text-grey-2 text-normal mt2 mb4", {"text-paragraph": smallDescription})}
+                style={{lineHeight: "1.5em"}}>{message}</h2>
         </div>
-        { action &&
-            <Link to={link} className="Button Button--primary mt3" target={link.startsWith('http') ? "_blank" : ""}>{action}</Link>
+        { action && link &&
+        <Link to={link} className="Button Button--primary mt4"
+              target={link.startsWith('http') ? "_blank" : ""}>{action}</Link>
+        }
+        { action && onActionClick &&
+        <a onClick={onActionClick} className="Button Button--primary mt4">{action}</a>
         }
     </div>
 
-EmptyState.propTypes = {
-    title:      PropTypes.string,
-    message:    PropTypes.string.isRequired,
-    icon:       PropTypes.string,
-    image:      PropTypes.string,
-    action:     PropTypes.string,
-    link:       PropTypes.string,
-};
 
 export default EmptyState;
diff --git a/frontend/src/metabase/components/FilterWidget.jsx b/frontend/src/metabase/components/FilterWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e8d58f7dc76ee6bc48df44d776d3c2814fee8c1b
--- /dev/null
+++ b/frontend/src/metabase/components/FilterWidget.jsx
@@ -0,0 +1,70 @@
+/* @flow */
+
+// An UI element that is normally right-aligned and showing a currently selected filter together with chevron.
+// Clicking the element will trigger a popover showing all available filter options.
+
+import React, { Component } from "react";
+import Icon from "metabase/components/Icon";
+import PopoverWithTrigger from "./PopoverWithTrigger";
+
+type FilterWidgetItem = {
+    id: string,
+    name: string,
+    icon: string
+}
+
+export default class FilterWidget extends Component {
+    props: {
+        items: FilterWidgetItem[],
+        activeItem: FilterWidgetItem,
+        onChange: (FilterWidgetItem) => void
+    };
+
+    popoverRef: PopoverWithTrigger;
+    iconRef: Icon;
+
+    render() {
+        const { items, activeItem, onChange } = this.props;
+        return (
+            <PopoverWithTrigger
+                ref={p => this.popoverRef = p}
+                triggerClasses="block ml-auto flex-no-shrink"
+                targetOffsetY={10}
+                triggerElement={
+                    <div className="ml2 flex align-center text-brand">
+                        <span className="text-bold">{activeItem && activeItem.name}</span>
+                        <Icon
+                            ref={i => this.iconRef = i}
+                            className="ml1"
+                            name="chevrondown"
+                            width="12"
+                            height="12"
+                        />
+                    </div>
+                }
+                target={() => this.iconRef}
+            >
+                <ol className="text-brand mt2 mb1">
+                    { items.map((item, index) =>
+                        <li
+                            key={index}
+                            className="cursor-pointer flex align-center brand-hover px2 py1 mb1"
+                            onClick={() => {
+                                onChange(item);
+                                this.popoverRef.close();
+                            }}
+                        >
+                            <Icon
+                                className="mr1 text-light-blue"
+                                name={item.icon}
+                            />
+                            <h4 className="List-item-title">
+                                {item.name}
+                            </h4>
+                        </li>
+                    ) }
+                </ol>
+            </PopoverWithTrigger>
+        )
+    }
+}
diff --git a/frontend/src/metabase/components/HeaderBar.jsx b/frontend/src/metabase/components/HeaderBar.jsx
index b12316ae08722d8e590d6e572cc85d62e14b0683..2f8c804cea75032db3c2fa9c1821ef6acca71d9e 100644
--- a/frontend/src/metabase/components/HeaderBar.jsx
+++ b/frontend/src/metabase/components/HeaderBar.jsx
@@ -42,10 +42,10 @@ export default class Header extends Component {
         }
 
         return (
-            <div className={cx("QueryBuilder-section pt2 sm-pt0 flex align-center", className)}>
+            <div className={cx("QueryBuilder-section pt2 sm-pt2 flex align-center", className)}>
                 <div className={cx("px2 sm-px0 pt2 relative flex-full")}>
                     { badge &&
-                        <div className="absolute top left ml2 sm-ml0">{badge}</div>
+                        <div className="absolute top left ml2 sm-ml2">{badge}</div>
                     }
                     {titleAndDescription}
                 </div>
diff --git a/frontend/src/metabase/components/HistoryModal.jsx b/frontend/src/metabase/components/HistoryModal.jsx
index 3b545724611c3afbb110586d732256c215b38da5..8d7e02799e021b412d0790dbc7402119aa69fc25 100644
--- a/frontend/src/metabase/components/HistoryModal.jsx
+++ b/frontend/src/metabase/components/HistoryModal.jsx
@@ -76,7 +76,7 @@ export default class HistoryModal extends Component {
                 title="Revision history"
                 onClose={() => this.props.onClose()}
             >
-                <LoadingAndErrorWrapper loading={!revisions} error={this.state.error}>
+                <LoadingAndErrorWrapper className="flex flex-full flex-basis-auto" loading={!revisions} error={this.state.error}>
                 {() =>
                     <div className="pb4 flex-full">
                         <div className="border-bottom flex px4 py1 text-uppercase text-grey-3 text-bold h5">
diff --git a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
index a0fc95eae3c38bc1107a25b7cd74f5569dfd811c..c5fd86d101bbcf8736044228f4230f1de6ce0212 100644
--- a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
+++ b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
@@ -60,7 +60,7 @@ export default class LoadingAndErrorWrapper extends Component {
             <div className={this.props.className} style={this.props.style}>
                 { error ?
                     <div className={contentClassName}>
-                        <h2 className="text-normal text-grey-2">{this.getErrorMessage()}</h2>
+                        <h2 className="text-normal text-grey-2 ie-wrap-content-fix">{this.getErrorMessage()}</h2>
                     </div>
                 : loading ?
                     <div className={contentClassName}>
diff --git a/frontend/src/metabase/components/Modal.jsx b/frontend/src/metabase/components/Modal.jsx
index 04d97740acfd632b116169f0f7c4626b492199f8..69a77183c8770429d4288d2cea154f4f064aa922 100644
--- a/frontend/src/metabase/components/Modal.jsx
+++ b/frontend/src/metabase/components/Modal.jsx
@@ -143,8 +143,12 @@ export class FullPageModal extends Component {
         this._renderModal(false);
 
         // restore scroll position and scrolling
-        window.scrollTo(this._scrollX, this._scrollY);
-        document.body.style.overflow = "unset";
+        document.body.style.overflow = "";
+
+        // On IE11 a timeout is required for the scroll to happen after the change of overflow setting
+        setTimeout(() => {
+            window.scrollTo(this._scrollX, this._scrollY);
+        }, 0)
 
         // wait for animations to complete before unmounting
         setTimeout(() => {
diff --git a/frontend/src/metabase/components/ModalContent.css b/frontend/src/metabase/components/ModalContent.css
deleted file mode 100644
index 4a7f4dde6a6bf386081ece957df525835311699e..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/ModalContent.css
+++ /dev/null
@@ -1,4 +0,0 @@
-/* remove padding since ModalContent has it's own */
-.ModalContent .Form-inputs {
-  /*padding: 0;*/
-}
diff --git a/frontend/src/metabase/components/ModalContent.jsx b/frontend/src/metabase/components/ModalContent.jsx
index 51909fcdea445bb91926073cd2fb473f8aa564d8..59b4b43f68af511794f310fabf592dbb2c238047 100644
--- a/frontend/src/metabase/components/ModalContent.jsx
+++ b/frontend/src/metabase/components/ModalContent.jsx
@@ -6,8 +6,6 @@ import Icon from "metabase/components/Icon.jsx";
 
 import cx from "classnames";
 
-import "./ModalContent.css";
-
 export default class ModalContent extends Component {
     static propTypes = {
         id: PropTypes.string,
@@ -66,7 +64,7 @@ ModalHeader.contextTypes = MODAL_CHILD_CONTEXT_TYPES;
 
 export const ModalBody = ({ children }, { fullPageModal, formModal }) =>
     <div
-        className={cx("ModalBody", { "px4": formModal, "flex flex-full": !formModal })}
+        className={cx("ModalBody", { "px4": formModal, "flex flex-full flex-basis-auto": !formModal })}
     >
         <div
             className="flex-full ml-auto mr-auto flex flex-column"
diff --git a/frontend/src/metabase/questions/components/SearchHeader.css b/frontend/src/metabase/components/SearchHeader.css
similarity index 86%
rename from frontend/src/metabase/questions/components/SearchHeader.css
rename to frontend/src/metabase/components/SearchHeader.css
index 3983646aaecb46f6f7e43eb9f56730f46186800e..f053bae23692cbfa6121066151dac76d9380ad05 100644
--- a/frontend/src/metabase/questions/components/SearchHeader.css
+++ b/frontend/src/metabase/components/SearchHeader.css
@@ -1,4 +1,4 @@
-@import '../Questions.css';
+@import '../questions/Questions.css';
 
 :local(.searchHeader) {
     composes: flex align-center from "style";
@@ -6,7 +6,6 @@
 
 :local(.searchIcon) {
     color: var(--muted-color);
-    margin-left: 10px;
 }
 
 :local(.searchBox) {
diff --git a/frontend/src/metabase/questions/components/SearchHeader.jsx b/frontend/src/metabase/components/SearchHeader.jsx
similarity index 100%
rename from frontend/src/metabase/questions/components/SearchHeader.jsx
rename to frontend/src/metabase/components/SearchHeader.jsx
diff --git a/frontend/src/metabase/components/SortableItemList.jsx b/frontend/src/metabase/components/SortableItemList.jsx
index 417eaacacff8a95e844e15f1308af066b58a7fdc..0a0f61cf4131e8d2aba421c245d751d6e2513b2f 100644
--- a/frontend/src/metabase/components/SortableItemList.jsx
+++ b/frontend/src/metabase/components/SortableItemList.jsx
@@ -61,10 +61,12 @@ export default class SortableItemList extends Component {
                                         <h4 className="text-grey-3">{item.description || "No description yet"}</h4>
                                     </div>
                                 </div>
-                                <div className="flex-align-right text-right text-grey-3">
-                                    <div className="mb1">Saved by {item.creator.common_name}</div>
-                                    <div>Modified {item.updated_at.fromNow()}</div>
-                                </div>
+                                {item.creator && item.updated_at &&
+                                    <div className="flex-align-right text-right text-grey-3">
+                                        <div className="mb1">Saved by {item.creator.common_name}</div>
+                                        <div>Modified {item.updated_at.fromNow()}</div>
+                                    </div>
+                                }
                             </a>
                         </li>
                     )}
diff --git a/frontend/src/metabase/components/TitleAndDescription.jsx b/frontend/src/metabase/components/TitleAndDescription.jsx
index 899de18e87ff7f913f8486f7bfd1b24df7e6f346..2099c33a3fda6fa6cee99ef86ee8a561775109ac 100644
--- a/frontend/src/metabase/components/TitleAndDescription.jsx
+++ b/frontend/src/metabase/components/TitleAndDescription.jsx
@@ -1,12 +1,18 @@
+/* @flow */
 import React from 'react';
-import PropTypes from "prop-types";
+import cx from "classnames";
 import pure from "recompose/pure";
 
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
-const TitleAndDescription = ({ title, description }) =>
-    <div className="flex align-center">
+type Attributes = {
+    title: string,
+    description?: string,
+    className?: string
+}
+const TitleAndDescription = ({ title, description, className }: Attributes) =>
+    <div className={cx("flex align-center", className)}>
         <h2 className="mr1">{title}</h2>
         { description &&
             <Tooltip tooltip={description} maxWidth={'22em'}>
@@ -15,9 +21,4 @@ const TitleAndDescription = ({ title, description }) =>
         }
     </div>;
 
-TitleAndDescription.propTypes = {
-    title: PropTypes.string.isRequired,
-    description: PropTypes.string
-};
-
 export default pure(TitleAndDescription);
diff --git a/frontend/src/metabase/components/icons/NightModeIcon.jsx b/frontend/src/metabase/components/icons/NightModeIcon.jsx
index 5d5010b41dec581951a690073f6e12df80e4e912..dcca140f9e5c04d467b8889c9a74ab35cbf60112 100644
--- a/frontend/src/metabase/components/icons/NightModeIcon.jsx
+++ b/frontend/src/metabase/components/icons/NightModeIcon.jsx
@@ -1,8 +1,16 @@
+/* @flow */
+
 import React from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const NightModeIcon = (props) =>
-    <Icon name={props.isNightMode ? "sun" : "moon"} {...props} />
+type Props = {
+    // ...IconProps,
+    isNightMode: boolean
+};
+
+const NightModeIcon = ({ isNightMode, ...props }: Props) => (
+    <Icon name={isNightMode ? "sun" : "moon"} {...props} />
+);
 
 export default NightModeIcon;
diff --git a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx b/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx
similarity index 55%
rename from frontend/src/metabase/components/AddToDashSelectDashModal.jsx
rename to frontend/src/metabase/containers/AddToDashSelectDashModal.jsx
index 64d6ef600c945e7326c547cbe7e48b15ff454663..182384687bfa8a4e624a1b48e6fe2c5353f2bd00 100644
--- a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
+++ b/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx
@@ -1,62 +1,69 @@
+/* @flow  */
+
 import React, { Component } from "react";
-import PropTypes from "prop-types";
+import {connect} from "react-redux";
 
 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 * as Urls from "metabase/lib/urls";
-import { DashboardApi } from "metabase/services";
 
-import moment from 'moment';
+import * as dashboardsActions from 'metabase/dashboards/dashboards';
+import { getDashboardListing } from 'metabase/dashboards/selectors';
 
-export default class AddToDashSelectDashModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.addToDashboard = this.addToDashboard.bind(this);
-        this.createDashboard = this.createDashboard.bind(this);
-        this.loadDashboardList();
+import type { Dashboard } from 'metabase/meta/types/Dashboard'
+import type { Card } from 'metabase/meta/types/Card'
+const mapStateToProps = (state) => ({
+    dashboards: getDashboardListing(state)
+});
 
-        this.state = {
-            dashboards: null,
-            shouldCreateDashboard: false
-        };
-    }
+const mapDispatchToProps = {
+    fetchDashboards: dashboardsActions.fetchDashboards,
+    createDashboard: dashboardsActions.createDashboard
+};
 
-    static propTypes = {
-        card: PropTypes.object.isRequired,
-        onClose: PropTypes.func.isRequired,
-        onChangeLocation: PropTypes.func.isRequired
+@connect(mapStateToProps, mapDispatchToProps)
+export default class AddToDashSelectDashModal extends Component {
+    state = {
+        shouldCreateDashboard: false
     };
 
-    async loadDashboardList() {
-        // TODO: reduxify
-        var dashboards = await DashboardApi.list({ f: "all" });
-        for (var dashboard of dashboards) {
-            dashboard.updated_at = moment(dashboard.updated_at);
-        }
-        this.setState({ dashboards });
+    props: {
+        card: Card,
+        onClose: () => void,
+        onChangeLocation: (string) => void,
+        // via connect:
+        dashboards: Dashboard[],
+        fetchDashboards: () => any,
+        createDashboard: (Dashboard) => any
+    };
+
+    componentWillMount() {
+        this.props.fetchDashboards();
     }
 
-    addToDashboard(dashboard) {
+    addToDashboard = (dashboard: Dashboard) => {
         // we send the user over to the chosen dashboard in edit mode with the current card added
         this.props.onChangeLocation(Urls.dashboard(dashboard.id)+"?add="+this.props.card.id);
     }
 
-    async createDashboard(newDashboard) {
-        // TODO: reduxify
-        let dashboard = await DashboardApi.create(newDashboard);
-        // this.props.notifyDashboardCreatedFn(dashboard);
-        this.addToDashboard(dashboard);
+    createDashboard = async(newDashboard: Dashboard) => {
+        try {
+            const action = await this.props.createDashboard(newDashboard, {});
+            this.addToDashboard(action.payload);
+        } catch (e) {
+            console.log("createDashboard failed", e);
+        }
     }
 
     render() {
-        if (!this.state.dashboards) {
-            return null;
-        } else if (this.state.dashboards.length === 0 || this.state.shouldCreateDashboard === true) {
+        if (this.props.dashboards === null) {
+            return <div></div>;
+        } else if (this.props.dashboards.length === 0 || this.state.shouldCreateDashboard === true) {
             return <CreateDashboardModal createDashboardFn={this.createDashboard} onClose={this.props.onClose} />
         } else {
+            console.log('in this branch, confusing', this.state, this.props)
             return (
                 <ModalContent
                     id="AddToDashSelectDashModal"
@@ -77,7 +84,7 @@ export default class AddToDashSelectDashModal extends Component {
                         </div>
                     </div>
                     <SortableItemList
-                        items={this.state.dashboards}
+                        items={this.props.dashboards}
                         onClickItemFn={this.addToDashboard}
                     />
                 </div>
diff --git a/frontend/src/metabase/css/components/buttons.css b/frontend/src/metabase/css/components/buttons.css
index 45275e8d3d145ba35f8f80625d07fb6245963a84..b6ee88db59da09f745f02b1f8a64d26983196b53 100644
--- a/frontend/src/metabase/css/components/buttons.css
+++ b/frontend/src/metabase/css/components/buttons.css
@@ -24,7 +24,7 @@
   cursor: pointer;
   text-decoration: none;
   font-weight: bold;
-  font-family: "Lato";
+  font-family: "Lato", sans-serif;
   border-radius: var(--default-button-border-radius);
 }
 
diff --git a/frontend/src/metabase/css/components/modal.css b/frontend/src/metabase/css/components/modal.css
index 1cc813e3dfbe8d8763cb6274f176acd245d917a7..7ee88dad85188886bf0f5eb4b444e3b4a7c3c648 100644
--- a/frontend/src/metabase/css/components/modal.css
+++ b/frontend/src/metabase/css/components/modal.css
@@ -10,6 +10,14 @@
   overflow-y: auto;
 }
 
+/* On IE11, single flex item with `margin: auto` gets shifted to flex end
+ * https://github.com/philipwalton/flexbugs/issues/157
+ * Set margin to zero when using Flexbox in `WindowModal` component 
+ */
+.Modal-backdrop > .Modal, .Modal-backdrop--dark > .Modal {
+  margin: 0;
+}
+
 .Modal.Modal--small   { width: 480px; } /* TODO - why is this one px? */
 .Modal.Modal--medium  { width: 65%; }
 .Modal.Modal--wide    { width: 85%; }
@@ -32,7 +40,7 @@
     background-color: rgba(255, 255, 255, 0.6);
 }
 
-.Modal-backdrop-dark {
+.Modal-backdrop--dark {
     background-color: rgba(75, 75, 75, 0.7);
 }
 
diff --git a/frontend/src/metabase/css/core/base.css b/frontend/src/metabase/css/core/base.css
index e1bb63e39e50519046762bc44969bc28902b577d..6ed9b2935a3893690157e4e8a137c034fb17492f 100644
--- a/frontend/src/metabase/css/core/base.css
+++ b/frontend/src/metabase/css/core/base.css
@@ -6,6 +6,7 @@
 
 html {
     height: 100%; /* ensure the entire page will fill the window */
+    width: 100%;
 }
 
 body {
diff --git a/frontend/src/metabase/css/core/box_sizing.css b/frontend/src/metabase/css/core/box_sizing.css
index 9191b9c644423e86af63312ea7524774e3a4ae9d..0d39ed63ae5a61f13afeacd2bf99281c9d92eba8 100644
--- a/frontend/src/metabase/css/core/box_sizing.css
+++ b/frontend/src/metabase/css/core/box_sizing.css
@@ -15,5 +15,10 @@ textarea,
 ul,
 li,
 span {
-  box-sizing: border-box;
+    box-sizing: border-box;
 }
+
+/* for applying border-box to other elements on ad-hoc basis */
+.border-box {
+    box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/frontend/src/metabase/css/core/colors.css b/frontend/src/metabase/css/core/colors.css
index 2611c0550d0bb52795ca9270c6003d174241e0ec..0242b1d3f1481dc1a1601331b21432cc48c5ed19 100644
--- a/frontend/src/metabase/css/core/colors.css
+++ b/frontend/src/metabase/css/core/colors.css
@@ -7,6 +7,7 @@
   --grey-2: color(var(--base-grey) shade(20%));
   --grey-3: color(var(--base-grey) shade(30%));
   --grey-4: color(var(--base-grey) shade(40%));
+  --grey-5: color(var(--base-grey) shade(50%));
 
   --grey-text-color: #797979;
   --alt-color: #F5F7F9;
@@ -145,12 +146,16 @@
 .text-grey-4,
 .text-grey-4-hover:hover { color: var(--grey-4) }
 
+.text-grey-5,
+.text-grey-5-hover:hover { color: var(--grey-5) }
+
 .bg-grey-0,
 .bg-grey-0-hover:hover { background-color: var(--base-grey) }
 .bg-grey-1 { background-color: var(--grey-1) }
 .bg-grey-2 { background-color: var(--grey-2) }
 .bg-grey-3 { background-color: var(--grey-3) }
 .bg-grey-4 { background-color: var(--grey-4) }
+.bg-grey-5 { background-color: var(--grey-5) }
 
 .bg-slate { background-color: var(--slate-color); }
 .bg-slate-light { background-color: var(--slate-light-color); }
diff --git a/frontend/src/metabase/css/core/flex.css b/frontend/src/metabase/css/core/flex.css
index f88fcf8efb97e6ba990655bf4b5468b492994915..600321610734f0805c3244d8fa0544178565edd9 100644
--- a/frontend/src/metabase/css/core/flex.css
+++ b/frontend/src/metabase/css/core/flex.css
@@ -16,6 +16,29 @@
     flex-shrink: 0;
 }
 
+/* The behavior of how `flex: <flex-grow>` sets flex-basis is inconsistent across
+ * browsers. Specifically:
+ * - On Chrome and FF it's set to `flex-basis: 0%`. That behaves equally as `height: 0%`.
+ *   It means that if the containing block has no explicit height, then `height: 0%` is computed as `height: auto`,
+ *   and element grows as its content grows. That is the most common scenario in Metabase codebase.
+ * - On older IEs it's set to `flex-basis: 0` which means that the initial main size of flex item is zero.
+ *   It is also notable that `flex-basis: 0%` doesn't work correctly on IE.
+ *
+ *  As a solution, `flex-basis-auto` should always be used in conjunction with `flex-full` when it is
+ *  a desired behavior that the element grows with its contents.
+*/
+.flex-basis-auto {
+    flex-basis: auto;
+}
+
+.shrink-below-content-size {
+    /* W3C spec says:
+     * By default, flex items won’t shrink below their minimum content size (the length of the longest word or
+     * fixed-size element). To change this, set the min-width or min-height property.
+     */
+    min-width: 0;
+}
+
 .align-center, :local(.align-center) {
     align-items: center;
 }
@@ -127,9 +150,16 @@
 }
 
 .no-flex {
-    flex: 0;
+    flex: 0 1 0%;
 }
 
 @media screen and (--breakpoint-min-md) {
     .md-no-flex { flex: 0 !important; }
 }
+
+/* Contents of elements inside flex items might not be wrapped correctly on IE11,
+   set max-width manually to enforce wrapping
+*/
+.ie-wrap-content-fix {
+   max-width: 100%;
+}
\ No newline at end of file
diff --git a/frontend/src/metabase/css/core/index.css b/frontend/src/metabase/css/core/index.css
index fa94b8c9a1a50c6f20e7d24f6c8bb0af3cd3d341..212ede6b6f4518561bb0be9aa489ad6fafad8e7d 100644
--- a/frontend/src/metabase/css/core/index.css
+++ b/frontend/src/metabase/css/core/index.css
@@ -15,6 +15,7 @@
 @import './inputs.css';
 @import './layout.css';
 @import './link.css';
+@import './overflow.css';
 @import './rounded.css';
 @import './scroll.css';
 @import './shadow.css';
diff --git a/frontend/src/metabase/css/core/inputs.css b/frontend/src/metabase/css/core/inputs.css
index d727a683237d9cb7c91f75c047cf87ae3baef15c..78240bd472d4ca71df4e3835e365cba313c9db78 100644
--- a/frontend/src/metabase/css/core/inputs.css
+++ b/frontend/src/metabase/css/core/inputs.css
@@ -13,6 +13,13 @@
   transition: border .3s linear;
 }
 
+/* React doesn't receive events from IE11:s input clear button so don't show it */
+.input::-ms-clear {
+  display: none;
+  width: 0;
+  height: 0;
+}
+
 .input--small {
   padding: 0.3rem 0.4rem;
 }
diff --git a/frontend/src/metabase/css/core/overflow.css b/frontend/src/metabase/css/core/overflow.css
new file mode 100644
index 0000000000000000000000000000000000000000..66c551d2ab30f976a6832321810c8c4de48cd239
--- /dev/null
+++ b/frontend/src/metabase/css/core/overflow.css
@@ -0,0 +1,7 @@
+.overflow-auto {
+    overflow: auto;
+}
+
+.overflow-hidden {
+    overflow: hidden;
+}
\ No newline at end of file
diff --git a/frontend/src/metabase/css/core/spacing.css b/frontend/src/metabase/css/core/spacing.css
index bd616e03765ae2b815f3eaf0d0277f05719f7ca8..57d5c9654d9e6e540c2c6cb04793e5c6b7a2f44d 100644
--- a/frontend/src/metabase/css/core/spacing.css
+++ b/frontend/src/metabase/css/core/spacing.css
@@ -186,6 +186,11 @@
 .ml4, :local(.ml4) { margin-left:   var(--margin-4); }
 .mr4, :local(.mr4) { margin-right:  var(--margin-4); }
 
+/* negative margin (mainly for correction of horizontal positioning) */
+.mln1 { margin-left: -var(--margin-1) }
+.mln2 { margin-left: -var(--margin-2) }
+.mln3 { margin-left: -var(--margin-3) }
+.mln4 { margin-left: -var(--margin-4) }
 
 /* responsive spacing */
 
diff --git a/frontend/src/metabase/css/core/text.css b/frontend/src/metabase/css/core/text.css
index 47cce64492670c0fbcd4305978dd6d832ea3d15e..6aa0298d8a374335727bbb9bb346be70be4f4d8a 100644
--- a/frontend/src/metabase/css/core/text.css
+++ b/frontend/src/metabase/css/core/text.css
@@ -68,6 +68,8 @@
 }
 .text-lowercase { text-transform: lowercase; }
 
+.text-capitalize { text-transform: capitalize; }
+
 /* text weight */
 .text-light  { font-weight: 300; }
 .text-normal { font-weight: 400; }
@@ -94,6 +96,10 @@
     font-size: 0.875em;
 }
 
+.text-smaller {
+    font-size: 0.8em;
+}
+
 .text-current {
     color: currentColor;
 }
diff --git a/frontend/src/metabase/css/home.css b/frontend/src/metabase/css/home.css
index 33b6893ca007424e3244778eda0edbb51b799ce8..2e68929d33a2d43a361021fc0cc2358ee597ca33 100644
--- a/frontend/src/metabase/css/home.css
+++ b/frontend/src/metabase/css/home.css
@@ -2,15 +2,6 @@
     z-index: 4;
 }
 
-.CheckBg {
-    background-image: url('../assets/img/header_rect.svg');
-    background-repeat: repeat;
-}
-
-.CheckBg-offset {
-  background-position-y: -15px;
-}
-
 .NavItem {
     border-radius: 8px;
 }
diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx
index eab79d5f7cc7105b0871ad031dab03584756b4f4..8f292bac556e0f86eb11c17636c09acc8c2f6213 100644
--- a/frontend/src/metabase/dashboard/components/DashCard.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCard.jsx
@@ -10,7 +10,7 @@ import ChartSettings from "metabase/visualizations/components/ChartSettings.jsx"
 
 import Icon from "metabase/components/Icon.jsx";
 
-import DashCardParameterMapper from "../components/parameters/DashCardParameterMapper.jsx";
+import DashCardParameterMapper from "./DashCardParameterMapper.jsx";
 
 import { IS_EMBED_PREVIEW } from "metabase/lib/embed";
 
@@ -28,6 +28,7 @@ export default class DashCard extends Component {
     static propTypes = {
         dashcard: PropTypes.object.isRequired,
         dashcardData: PropTypes.object.isRequired,
+        cardDurations: PropTypes.object.isRequired,
         parameterValues: PropTypes.object.isRequired,
         markNewCardSeen: PropTypes.func.isRequired,
         fetchCardData: PropTypes.func.isRequired,
@@ -110,7 +111,7 @@ export default class DashCard extends Component {
 
         return (
             <div
-                className={"Card bordered rounded flex flex-column hover-parent hover--visibility" + cx({
+                className={cx("Card bordered rounded flex flex-column hover-parent hover--visibility", {
                     "Card--recent": dashcard.isAdded,
                     "Card--unmapped": !isMappedToAllParameters && !isEditing,
                     "Card--slow": isSlow === "usually-slow"
diff --git a/frontend/src/metabase/dashboard/components/DashCard.spec.js b/frontend/src/metabase/dashboard/components/DashCard.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3debcfc1fdc5766494ecc6438dd757308b3917c1
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashCard.spec.js
@@ -0,0 +1,55 @@
+import React from "react";
+import renderer from "react-test-renderer";
+import { render } from "enzyme";
+import { assocIn } from "icepick";
+
+import DashCard from "./DashCard";
+
+jest.mock("metabase/visualizations/components/Visualization.jsx");
+
+const DEFAULT_PROPS = {
+    dashcard: {
+        card: { id: 1 },
+        series: [],
+        parameter_mappings: []
+    },
+    dashcardData: {
+        1: { cols: [], rows: [] }
+    },
+    cardDurations: {},
+    parameterValues: {},
+    markNewCardSeen: () => {},
+    fetchCardData: () => {}
+};
+
+describe("DashCard", () => {
+    it("should render with no special classNames", () => {
+        expect(
+            renderer.create(<DashCard {...DEFAULT_PROPS} />).toJSON()
+        ).toMatchSnapshot();
+    });
+    it("should render unmapped card with Card--unmapped className", () => {
+        const props = assocIn(DEFAULT_PROPS, ["parameterValues", "foo"], "bar");
+        const dashCard = render(<DashCard {...props} />);
+        expect(dashCard.find(".Card--recent")).toHaveLength(0);
+        expect(dashCard.find(".Card--unmapped")).toHaveLength(1);
+        expect(dashCard.find(".Card--slow")).toHaveLength(0);
+    });
+    it("should render slow card with Card--slow className", () => {
+        const props = assocIn(DEFAULT_PROPS, ["cardDurations", 1], {
+            average: 1,
+            fast_threshold: 1
+        });
+        const dashCard = render(<DashCard {...props} />);
+        expect(dashCard.find(".Card--recent")).toHaveLength(0);
+        expect(dashCard.find(".Card--unmapped")).toHaveLength(0);
+        expect(dashCard.find(".Card--slow")).toHaveLength(1);
+    });
+    it("should render new card with Card--recent className", () => {
+        const props = assocIn(DEFAULT_PROPS, ["dashcard", "isAdded"], true);
+        const dashCard = render(<DashCard {...props} />);
+        expect(dashCard.find(".Card--recent")).toHaveLength(1);
+        expect(dashCard.find(".Card--unmapped")).toHaveLength(0);
+        expect(dashCard.find(".Card--slow")).toHaveLength(0);
+    });
+});
diff --git a/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx b/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx
similarity index 89%
rename from frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx
rename to frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx
index 00a5bc7476a71085a655458bb948e8add226d23b..70981453d08a0228dfa51720030df69bfa9e57c0 100644
--- a/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx
@@ -1,6 +1,6 @@
 import React from "react";
 
-import DashCardCardParameterMapper from "../../containers/DashCardCardParameterMapper.jsx";
+import DashCardCardParameterMapper from "../containers/DashCardCardParameterMapper.jsx";
 
 const DashCardParameterMapper = ({ dashcard }) =>
     <div className="relative flex-full flex flex-column layout-centered">
diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx
index 5325abcbdd86492c2bb0a301d9bede809b99ecc6..578c71a8f6d979baefde09b79a4077aa2305a432 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard.jsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx
@@ -1,42 +1,85 @@
+/* @flow */
+
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import DashboardHeader from "../components/DashboardHeader.jsx";
 import DashboardGrid from "../components/DashboardGrid.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
-import MetabaseAnalytics from "metabase/lib/analytics";
 
-import Parameters from "../containers/Parameters.jsx";
+import Parameters from "metabase/parameters/components/Parameters.jsx";
 
-import screenfull from "screenfull";
+import DashboardControls from "../hoc/DashboardControls";
 
 import _ from "underscore";
 import cx from "classnames";
-import querystring from "querystring";
 
-const TICK_PERIOD = 0.25; // seconds
+import type { LocationDescriptor, ApiError, QueryParams } from "metabase/meta/types"
 
-export default class Dashboard extends Component {
+import type { Card, CardId, VisualizationSettings } from "metabase/meta/types/Card";
+import type { DashboardWithCards, DashboardId, DashCardId } from "metabase/meta/types/Dashboard";
+import type { RevisionId } from "metabase/meta/types/Revision";
+import type { Parameter, ParameterId, ParameterValues, ParameterOption } from "metabase/meta/types/Parameter";
 
-    constructor(props, context) {
-        super(props, context);
+type Props = {
+    location:               LocationDescriptor,
 
-        this.state = {
-            error: null,
+    dashboardId:            DashboardId,
+    dashboard:              DashboardWithCards,
+    cards:                  Card[],
 
-            isFullscreen: false,
-            isNightMode: false,
+    isEditable:             boolean,
+    isEditing:              boolean,
+    isEditingParameter:     boolean,
 
-            refreshPeriod: null,
-            refreshElapsed: null
-        };
+    parameters:             Parameter[],
+    parameterValues:        ParameterValues,
 
-        _.bindAll(this,
-            "setRefreshPeriod", "tickRefreshClock",
-            "setFullscreen", "setNightMode", "fullScreenChanged",
-            "setEditing", "setDashboardAttribute",
-        );
-    }
+    addCardOnLoad:          DashboardId,
+
+    initialize:             () => Promise<void>,
+    addCardToDashboard:     ({ dashId: DashCardId, cardId: CardId }) => void,
+    deleteDashboard:        (dashboardId: DashboardId) => void,
+    fetchCards:             (filterMode?: string) => void,
+    fetchDashboard:         (dashboardId: DashboardId, queryParams: ?QueryParams) => void,
+    fetchRevisions:         ({ entity: string, id: number }) => void,
+    revertToRevision:       ({ entity: string, id: number, revision_id: RevisionId }) => void,
+    saveDashboardAndCards:  () => Promise<void>,
+    setDashboardAttributes: ({ [attribute: string]: any }) => void,
+    fetchDashboardCardData: (options: { reload: bool, clear: bool }) => Promise<void>,
+
+    setEditingParameter:    (parameterId: ParameterId) => void,
+    setEditingDashboard:    (isEditing: boolean) => void,
+
+    addParameter:               (option: ParameterOption) => Promise<Parameter>,
+    removeParameter:            (parameterId: ParameterId) => void,
+    setParameterName:           (parameterId: ParameterId, name: string) => void,
+    setParameterValue:          (parameterId: ParameterId, value: string) => void,
+    setParameterDefaultValue:   (parameterId: ParameterId, defaultValue: string) => void,
+
+    editingParameter:       ?Parameter,
+
+    isFullscreen:           boolean,
+    isNightMode:            boolean,
+    onRefreshPeriodChange:  (?number) => void,
+    loadDashboardParams:    () => void,
+
+    onReplaceAllDashCardVisualizationSettings: (dashcardId: DashCardId, settings: VisualizationSettings) => void,
+    onUpdateDashCardVisualizationSettings: (dashcardId: DashCardId, settings: VisualizationSettings) => void,
+
+    onChangeLocation:       (string) => void,
+    setErrorPage:           (error: ApiError) => void,
+}
+
+type State = {
+    error: ?ApiError
+}
+
+@DashboardControls
+export default class Dashboard extends Component<*, Props, State> {
+    state = {
+        error: null,
+    };
 
     static propTypes = {
         isEditable: PropTypes.bool,
@@ -45,6 +88,7 @@ export default class Dashboard extends Component {
 
         dashboard: PropTypes.object,
         cards: PropTypes.array,
+        parameters: PropTypes.array,
 
         addCardToDashboard: PropTypes.func.isRequired,
         deleteDashboard: PropTypes.func.isRequired,
@@ -52,7 +96,7 @@ export default class Dashboard extends Component {
         fetchDashboard: PropTypes.func.isRequired,
         fetchRevisions: PropTypes.func.isRequired,
         revertToRevision: PropTypes.func.isRequired,
-        saveDashboard: PropTypes.func.isRequired,
+        saveDashboardAndCards: PropTypes.func.isRequired,
         setDashboardAttributes: PropTypes.func.isRequired,
         setEditingDashboard: PropTypes.func.isRequired,
 
@@ -66,50 +110,22 @@ export default class Dashboard extends Component {
         isEditable: true
     };
 
-    async componentDidMount() {
-        this.loadDashboard(this.props.params.dashboardId);
-    }
-
-    componentDidUpdate() {
-        this.updateParams();
-        this._showNav(!this.state.isFullscreen);
+    componentDidMount() {
+        this.loadDashboard(this.props.dashboardId);
     }
 
-    componentWillReceiveProps(nextProps) {
-        if (this.props.params.dashboardId !== nextProps.params.dashboardId) {
-            this.loadDashboard(nextProps.params.dashboardId);
+    componentWillReceiveProps(nextProps: Props) {
+        if (this.props.dashboardId !== nextProps.dashboardId) {
+            this.loadDashboard(nextProps.dashboardId);
         } else if (!_.isEqual(this.props.parameterValues, nextProps.parameterValues) || !this.props.dashboard) {
             this.props.fetchDashboardCardData({ reload: false, clear: true });
         }
     }
 
-    componentWillMount() {
-        if (screenfull.enabled) {
-            document.addEventListener(screenfull.raw.fullscreenchange, this.fullScreenChanged);
-        }
-    }
-
-    componentWillUnmount() {
-        this._showNav(true);
-        this._clearRefreshInterval();
-        if (screenfull.enabled) {
-            document.removeEventListener(screenfull.raw.fullscreenchange, this.fullScreenChanged);
-        }
-    }
-
-    _showNav(show) {
-        const nav = document.querySelector(".Nav");
-        if (show && nav) {
-            nav.classList.remove("hide");
-        } else if (!show && nav) {
-            nav.classList.add("hide");
-        }
-    }
-
-    async loadDashboard(dashboardId) {
+    async loadDashboard(dashboardId: DashboardId) {
         this.props.initialize();
 
-        this.loadParams();
+        this.props.loadDashboardParams();
         const { addCardOnLoad, fetchDashboard, fetchCards, addCardToDashboard, setErrorPage, location } = this.props;
 
         try {
@@ -130,103 +146,26 @@ export default class Dashboard extends Component {
         }
     }
 
-    loadParams() {
-        let params = querystring.parse(window.location.hash.substring(1));
-        let refresh = parseInt(params.refresh);
-        this.setRefreshPeriod(Number.isNaN(refresh) || refresh === 0 ? null : refresh);
-        this.setNightMode("night" in params);
-        this.setFullscreen("fullscreen" in params);
-    }
-
-    updateParams() {
-        let hashParams = {};
-        if (this.state.refreshPeriod) {
-            hashParams.refresh = this.state.refreshPeriod;
-        }
-        if (this.state.isFullscreen) {
-            hashParams.fullscreen = true;
-        }
-        if (this.state.isNightMode) {
-            hashParams.night = true;
-        }
-        let hash = querystring.stringify(hashParams).replace(/=true\b/g, "");
-        hash = (hash ? "#" + hash : "");
-
-        // setting window.location.hash = "" causes the page to reload for some reasonc
-        if (hash !== window.location.hash) {
-            history.replaceState(null, document.title, window.location.pathname + window.location.search + hash);
-        }
-    }
-
-    _clearRefreshInterval() {
-        if (this._interval != null) {
-            clearInterval(this._interval);
-        }
-    }
-
-    setRefreshPeriod(refreshPeriod) {
-        this._clearRefreshInterval();
-        if (refreshPeriod != null) {
-            this._interval = setInterval(this.tickRefreshClock, TICK_PERIOD * 1000);
-            this.setState({ refreshPeriod, refreshElapsed: 0 });
-            MetabaseAnalytics.trackEvent("Dashboard", "Set Refresh", refreshPeriod);
-        } else {
-            this.setState({ refreshPeriod: null, refreshElapsed: null });
-        }
-    }
-
-    setNightMode(isNightMode) {
-        this.setState({ isNightMode });
-    }
-
-    setFullscreen(isFullscreen, browserFullscreen = true) {
-        if (isFullscreen !== this.state.isFullscreen) {
-            if (screenfull.enabled && browserFullscreen) {
-                if (isFullscreen) {
-                    screenfull.request();
-                } else {
-                    screenfull.exit();
-                }
-            }
-            this.setState({ isFullscreen });
-        }
-    }
-
-    fullScreenChanged() {
-        this.setState({ isFullscreen: screenfull.isFullscreen });
-    }
-
-    setEditing(isEditing) {
-        this.setRefreshPeriod(null);
+    setEditing = (isEditing: boolean) => {
+        this.props.onRefreshPeriodChange(null);
         this.props.setEditingDashboard(isEditing);
     }
 
-    setDashboardAttribute(attribute, value) {
+    setDashboardAttribute = (attribute: string, value: any) => {
         this.props.setDashboardAttributes({
             id: this.props.dashboard.id,
             attributes: { [attribute]: value }
         });
     }
 
-    async tickRefreshClock() {
-        let refreshElapsed = (this.state.refreshElapsed || 0) + TICK_PERIOD;
-        if (refreshElapsed >= this.state.refreshPeriod) {
-            refreshElapsed = 0;
-
-            await this.props.fetchDashboard(this.props.params.dashboardId, this.props.location.query);
-            this.props.fetchDashboardCardData({ reload: true, clear: false });
-        }
-        this.setState({ refreshElapsed });
-    }
-
     render() {
-        let { dashboard, isEditing, editingParameter, parameterValues, location } = this.props;
-        let { error, isFullscreen, isNightMode } = this.state;
+        let { dashboard, isEditing, editingParameter, parameters, parameterValues, location, isFullscreen, isNightMode } = this.props;
+        let { error } = this.state;
         isNightMode = isNightMode && isFullscreen;
 
-        let parameters;
-        if (dashboard && dashboard.parameters && dashboard.parameters.length) {
-            parameters = (
+        let parametersWidget;
+        if (parameters && parameters.length > 0) {
+            parametersWidget = (
                 <Parameters
                     syncQueryString
 
@@ -234,7 +173,7 @@ export default class Dashboard extends Component {
                     isFullscreen={isFullscreen}
                     isNightMode={isNightMode}
 
-                    parameters={dashboard.parameters.map(p => ({ ...p, value: parameterValues[p.id] }))}
+                    parameters={parameters.map(p => ({ ...p, value: parameterValues[p.id] }))}
                     query={location.query}
 
                     editingParameter={editingParameter}
@@ -255,22 +194,15 @@ export default class Dashboard extends Component {
                     <header className="DashboardHeader relative z2">
                         <DashboardHeader
                             {...this.props}
-                            isFullscreen={this.state.isFullscreen}
-                            isNightMode={this.state.isNightMode}
-                            refreshPeriod={this.state.refreshPeriod}
-                            refreshElapsed={this.state.refreshElapsed}
-                            setRefreshPeriod={this.setRefreshPeriod}
-                            onFullscreenChange={this.setFullscreen}
-                            onNightModeChange={this.setNightMode}
                             onEditingChange={this.setEditing}
                             setDashboardAttribute={this.setDashboardAttribute}
                             addParameter={this.props.addParameter}
-                            parameters={parameters}
+                            parameters={parametersWidget}
                         />
                     </header>
-                    {!isFullscreen && parameters &&
+                    {!isFullscreen && parametersWidget &&
                         <div className="wrapper flex flex-column align-start mt2 relative z2">
-                            {parameters}
+                            {parametersWidget}
                         </div>
                     }
                     <div className="wrapper">
@@ -284,7 +216,6 @@ export default class Dashboard extends Component {
                         :
                             <DashboardGrid
                                 {...this.props}
-                                isFullscreen={this.state.isFullscreen}
                                 onEditingChange={this.setEditing}
                             />
                         }
diff --git a/frontend/src/metabase/dashboard/components/DashboardActions.jsx b/frontend/src/metabase/dashboard/components/DashboardActions.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f2d194ee1f9775b855d927fa2fc97779cd7f2ead
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashboardActions.jsx
@@ -0,0 +1,77 @@
+import React from "react";
+
+import Tooltip from "metabase/components/Tooltip";
+import NightModeIcon from "metabase/components/icons/NightModeIcon";
+import FullscreenIcon from "metabase/components/icons/FullscreenIcon";
+import RefreshWidget from "metabase/dashboard/components/RefreshWidget";
+
+export const getDashboardActions = (
+    {
+        isEditing = false,
+        isEmpty = false,
+        isFullscreen,
+        isNightMode,
+        onNightModeChange,
+        onFullscreenChange,
+        refreshPeriod,
+        refreshElapsed,
+        onRefreshPeriodChange
+    }
+) => {
+    const buttons = [];
+
+    if (!isEditing && !isEmpty) {
+        buttons.push(
+            <RefreshWidget
+                data-metabase-event="Dashboard;Refresh Menu Open"
+                className="text-brand-hover"
+                key="refresh"
+                period={refreshPeriod}
+                elapsed={refreshElapsed}
+                onChangePeriod={onRefreshPeriodChange}
+            />
+        );
+    }
+
+    if (!isEditing && isFullscreen) {
+        buttons.push(
+            <Tooltip tooltip={isNightMode ? "Daytime mode" : "Nighttime mode"}>
+                <span
+                    data-metabase-event={"Dashboard;Night Mode;" + !isNightMode}
+                >
+                    <NightModeIcon
+                        className="text-brand-hover cursor-pointer"
+                        key="night"
+                        isNightMode={isNightMode}
+                        onClick={() => onNightModeChange(!isNightMode)}
+                    />
+                </span>
+            </Tooltip>
+        );
+    }
+
+    if (!isEditing && !isEmpty) {
+        // option click to enter fullscreen without making the browser go fullscreen
+        buttons.push(
+            <Tooltip
+                tooltip={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
+            >
+                <span
+                    data-metabase-event={
+                        "Dashboard;Fullscreen Mode;" + !isFullscreen
+                    }
+                >
+                    <FullscreenIcon
+                        className="text-brand-hover cursor-pointer"
+                        key="fullscreen"
+                        isFullscreen={isFullscreen}
+                        onClick={e =>
+                            onFullscreenChange(!isFullscreen, !e.altKey)}
+                    />
+                </span>
+            </Tooltip>
+        );
+    }
+
+    return buttons;
+};
diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
index db1f7d977f87d32f2150aab9882267d9d519caee..37ca23df18f9fdf5d85cc6934ce04ca60e413af3 100644
--- a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
@@ -1,35 +1,79 @@
+/* @flow */
+
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
 import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.jsx";
 import DeleteDashboardModal from "./DeleteDashboardModal.jsx";
-import RefreshWidget from "./RefreshWidget.jsx";
 import Header from "metabase/components/Header.jsx";
 import HistoryModal from "metabase/components/HistoryModal.jsx";
-import FullscreenIcon from "metabase/components/icons/FullscreenIcon.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
-import NightModeIcon from "metabase/components/icons/NightModeIcon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import DashboardEmbedWidget from "../containers/DashboardEmbedWidget";
 
-import ParametersPopover from "./parameters/ParametersPopover.jsx";
+import { getDashboardActions } from "./DashboardActions";
+
+import ParametersPopover from "./ParametersPopover.jsx";
 import Popover from "metabase/components/Popover.jsx";
 
 import MetabaseSettings from "metabase/lib/settings";
 
 import cx from "classnames";
 
-export default class DashboardHeader extends Component {
+import type { LocationDescriptor, QueryParams, EntityType, EntityId } from "metabase/meta/types";
+import type { Card, CardId } from "metabase/meta/types/Card";
+import type { Parameter, ParameterId, ParameterOption } from "metabase/meta/types/Parameter";
+import type { DashboardWithCards, DashboardId, DashCardId } from "metabase/meta/types/Dashboard";
+import type { Revision, RevisionId } from "metabase/meta/types/Revision";
 
-    constructor(props, context) {
-        super(props, context);
+type Props = {
+    location:               LocationDescriptor,
 
-        this.state = {
-            modal: null,
-        };
-    }
+    dashboard:              DashboardWithCards,
+    cards:                  Card[],
+    revisions:              { [key: string]: Revision[] },
+
+    isAdmin:                boolean,
+    isEditable:             boolean,
+    isEditing:              boolean,
+    isFullscreen:           boolean,
+    isNightMode:            boolean,
+
+    refreshPeriod:          ?number,
+    refreshElapsed:         ?number,
+
+    parameters:             React$Element<*>[],
+
+    addCardToDashboard:     ({ dashId: DashCardId, cardId: CardId }) => void,
+    deleteDashboard:        (dashboardId: DashboardId) => void,
+    fetchCards:             (filterMode?: string) => void,
+    fetchDashboard:         (dashboardId: DashboardId, queryParams: ?QueryParams) => void,
+    fetchRevisions:         ({ entity: string, id: number }) => void,
+    revertToRevision:       ({ entity: string, id: number, revision_id: RevisionId }) => void,
+    saveDashboardAndCards:  () => Promise<void>,
+    setDashboardAttribute:  (attribute: string, value: any) => void,
+
+    addParameter:           (option: ParameterOption) => Promise<Parameter>,
+    setEditingParameter:    (parameterId: ?ParameterId) => void,
+
+    onEditingChange:        () => void,
+    onRefreshPeriodChange:  (?number) => void,
+    onNightModeChange:      (boolean) => void,
+    onFullscreenChange:     (boolean) => void,
+
+    onChangeLocation:       (string) => void,
+}
+
+type State = {
+    modal: null|"parameters",
+}
+
+export default class DashboardHeader extends Component<*, Props, State> {
+    state = {
+        modal: null,
+    };
 
     static propTypes = {
         dashboard: PropTypes.object.isRequired,
@@ -48,11 +92,11 @@ export default class DashboardHeader extends Component {
         fetchDashboard: PropTypes.func.isRequired,
         fetchRevisions: PropTypes.func.isRequired,
         revertToRevision: PropTypes.func.isRequired,
-        saveDashboard: PropTypes.func.isRequired,
+        saveDashboardAndCards: PropTypes.func.isRequired,
         setDashboardAttribute: PropTypes.func.isRequired,
-        onEditingChange: PropTypes.func.isRequired,
-        setRefreshPeriod: PropTypes.func.isRequired,
 
+        onEditingChange: PropTypes.func.isRequired,
+        onRefreshPeriodChange: PropTypes.func.isRequired,
         onNightModeChange: PropTypes.func.isRequired,
         onFullscreenChange: PropTypes.func.isRequired
     };
@@ -70,7 +114,7 @@ export default class DashboardHeader extends Component {
     }
 
     async onSave() {
-        await this.props.saveDashboard(this.props.dashboard.id);
+        await this.props.saveDashboardAndCards(this.props.dashboard.id);
         this.onDoneEditing();
     }
 
@@ -81,16 +125,16 @@ export default class DashboardHeader extends Component {
 
     async onDelete() {
         await this.props.deleteDashboard(this.props.dashboard.id);
-        this.props.onChangeLocation("/");
+        this.props.onChangeLocation("/dashboard");
     }
 
     // 1. fetch revisions
-    onFetchRevisions({ entity, id }) {
+    onFetchRevisions({ entity, id }: { entity: EntityType, id: EntityId }) {
         return this.props.fetchRevisions({ entity, id });
     }
 
     // 2. revert to a revision
-    onRevertToRevision({ entity, id, revision_id }) {
+    onRevertToRevision({ entity, id, revision_id }: { entity: EntityType, id: EntityId, revision_id: RevisionId }) {
         return this.props.revertToRevision({ entity, id, revision_id });
     }
 
@@ -130,7 +174,7 @@ export default class DashboardHeader extends Component {
     }
 
     getHeaderButtons() {
-        const { dashboard, parameters, isEditing, isFullscreen, isNightMode, isEditable, isAdmin } = this.props;
+        const { dashboard, parameters, isEditing, isFullscreen, isEditable, isAdmin } = this.props;
         const isEmpty = !dashboard || dashboard.ordered_cards.length === 0;
         const canEdit = isEditable && !!dashboard;
 
@@ -159,10 +203,10 @@ export default class DashboardHeader extends Component {
                     </Tooltip>
 
                     {this.state.modal && this.state.modal === "parameters" &&
-                        <Popover onClose={() => this.setState({modal: false})}>
+                        <Popover onClose={() => this.setState({ modal: null })}>
                             <ParametersPopover
                                 onAddParameter={this.props.addParameter}
-                                onClose={() => this.setState({modal: false})}
+                                onClose={() => this.setState({ modal: null })}
                             />
                         </Popover>
                     }
@@ -239,32 +283,7 @@ export default class DashboardHeader extends Component {
             )
         }
 
-        if (!isEditing && !isEmpty) {
-            buttons.push(
-                <RefreshWidget data-metabase-event="Dashboard;Refresh Menu Open" className="text-brand-hover" key="refresh" period={this.props.refreshPeriod} elapsed={this.props.refreshElapsed} onChangePeriod={this.props.setRefreshPeriod} />
-            );
-        }
-
-        if (!isEditing && isFullscreen) {
-            buttons.push(
-                <Tooltip tooltip={isNightMode ? "Daytime mode" : "Nighttime mode"}>
-                    <span data-metabase-event={"Dashboard;Night Mode;"+!isNightMode}>
-                        <NightModeIcon className="text-brand-hover cursor-pointer" key="night" isNightMode={isNightMode} onClick={() => this.props.onNightModeChange(!isNightMode) } />
-                    </span>
-                </Tooltip>
-            );
-        }
-
-        if (!isEditing && !isEmpty) {
-            // option click to enter fullscreen without making the browser go fullscreen
-            buttons.push(
-                <Tooltip tooltip={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}>
-                    <span data-metabase-event={"Dashboard;Fullscreen Mode;"+!isFullscreen}>
-                        <FullscreenIcon className="text-brand-hover cursor-pointer" key="fullscreen" isFullscreen={isFullscreen} onClick={(e) => this.props.onFullscreenChange(!isFullscreen, !e.altKey)} />
-                    </span>
-                </Tooltip>
-            );
-        }
+        buttons.push(...getDashboardActions(this.props));
 
         return [buttons];
     }
diff --git a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx b/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
similarity index 93%
rename from frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
rename to frontend/src/metabase/dashboard/components/ParametersPopover.jsx
index 636c2f92323db6b95b9b5d1fa3852171a407df27..85fa4f8e1cf84e07b02257266eadea0ab62e227a 100644
--- a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
+++ b/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
@@ -2,14 +2,15 @@
 import React, { Component } from "react";
 
 import { PARAMETER_SECTIONS } from "metabase/meta/Dashboard";
-import type { ParameterOption } from "metabase/meta/types/Dashboard";
+
+import type { Parameter, ParameterOption } from "metabase/meta/types/Parameter";
 
 import _ from "underscore";
 
 export default class ParametersPopover extends Component {
     props: {
-        onAddParameter: (option: ParameterOption) => {},
-        onClose: () => {}
+        onAddParameter: (option: ParameterOption) => Promise<Parameter>,
+        onClose: () => void
     };
     state: {
         section?: string
diff --git a/frontend/src/metabase/dashboard/components/__snapshots__/DashCard.spec.js.snap b/frontend/src/metabase/dashboard/components/__snapshots__/DashCard.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..19a455b1d1ef648fcf0ef54dc07f1011daad10ec
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/__snapshots__/DashCard.spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DashCard should render with no special classNames 1`] = `
+<div
+  className="Card bordered rounded flex flex-column hover-parent hover--visibility"
+/>
+`;
diff --git a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
index f2049bfce17699c3f815077a5854b736e3dd2239..c8b56fa32f7ccaadcffae900cd32679ba33a03a7 100644
--- a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
@@ -21,7 +21,8 @@ import cx from "classnames";
 import { getIn } from "icepick";
 
 import type { Card } from "metabase/meta/types/Card";
-import type { DashCard, Parameter, ParameterId, ParameterMappingUIOption, ParameterMappingTarget } from "metabase/meta/types/Dashboard";
+import type { DashCard } from "metabase/meta/types/Dashboard";
+import type { Parameter, ParameterId, ParameterMappingUIOption, ParameterTarget } from "metabase/meta/types/Parameter";
 import type { DatabaseId } from "metabase/meta/types/Database";
 
 import type { MappingsByParameter } from "../selectors";
@@ -50,12 +51,12 @@ export default class DashCardCardParameterMapper extends Component {
         card: Card,
         dashcard: DashCard,
         parameter: Parameter,
-        target: ParameterMappingTarget,
+        target: ParameterTarget,
         mappingOptions: Array<ParameterMappingUIOption>,
         mappingOptionSections: Array<Array<ParameterMappingUIOption>>,
         mappingsByParameter: MappingsByParameter,
         fetchDatabaseMetadata: (id: ?DatabaseId) => void,
-        setParameterMapping: (parameter_id: ParameterId, dashcard_id: number, card_id: number, target: ?ParameterMappingTarget) => void,
+        setParameterMapping: (parameter_id: ParameterId, dashcard_id: number, card_id: number, target: ?ParameterTarget) => void,
     };
 
     static propTypes = {
diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
index 568e01fefc0fa1fc671e1ce89d0ce0a76104fa86..4cf72c30f3fa96310459da861658654e7250c9a2 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
@@ -10,13 +10,16 @@ import Dashboard from "../components/Dashboard.jsx";
 import { fetchDatabaseMetadata } from "metabase/redux/metadata";
 import { setErrorPage } from "metabase/redux/app";
 
-import { getIsEditing, getIsEditingParameter, getIsDirty, getDashboardComplete, getCardList, getRevisions, getCardData, getCardDurations, getDatabases, getEditingParameter, getParameterValues } from "../selectors";
+import { getIsEditing, getIsEditingParameter, getIsDirty, getDashboardComplete, getCardList, getRevisions, getCardData, getCardDurations, getDatabases, getEditingParameter, getParameters, getParameterValues } from "../selectors";
 import { getUserIsAdmin } from "metabase/selectors/user";
 
 import * as dashboardActions from "../dashboard";
+import {deleteDashboard} from "metabase/dashboards/dashboards"
 
 const mapStateToProps = (state, props) => {
   return {
+      dashboardId:          props.params.dashboardId,
+
       isAdmin:              getUserIsAdmin(state, props),
       isEditing:            getIsEditing(state, props),
       isEditingParameter:   getIsEditingParameter(state, props),
@@ -28,13 +31,16 @@ const mapStateToProps = (state, props) => {
       cardDurations:        getCardDurations(state, props),
       databases:            getDatabases(state, props),
       editingParameter:     getEditingParameter(state, props),
+      parameters:           getParameters(state, props),
       parameterValues:      getParameterValues(state, props),
+
       addCardOnLoad:        props.location.query.add ? parseInt(props.location.query.add) : null
   }
 }
 
 const mapDispatchToProps = {
     ...dashboardActions,
+    deleteDashboard,
     fetchDatabaseMetadata,
     setErrorPage,
     onChangeLocation: push
diff --git a/frontend/src/metabase/dashboard/dashboard.js b/frontend/src/metabase/dashboard/dashboard.js
index 00467544c663d10eb7a228796adacf9fd0b4cb19..7ceb22a58f0be4270a2cd04f62213941870a399a 100644
--- a/frontend/src/metabase/dashboard/dashboard.js
+++ b/frontend/src/metabase/dashboard/dashboard.js
@@ -7,18 +7,20 @@ import moment from "moment";
 import { handleActions, combineReducers, createAction, createThunkAction } from "metabase/lib/redux";
 import { normalize, schema } from "normalizr";
 
+import { saveDashboard } from "metabase/dashboards/dashboards";
+
 import { createParameter, setParameterName as setParamName, setParameterDefaultValue as setParamDefaultValue } from "metabase/meta/Dashboard";
 import { applyParameters } from "metabase/meta/Card";
 import { getParametersBySlug } from "metabase/meta/Parameter";
 
-import type { Dashboard, DashCard, DashCardId } from "metabase/meta/types/Dashboard";
+import type { DashboardWithCards, DashCard, DashCardId } from "metabase/meta/types/Dashboard";
 import type { Card, CardId } from "metabase/meta/types/Card";
 
 import Utils from "metabase/lib/utils";
-import MetabaseAnalytics from "metabase/lib/analytics";
 import { getPositionForNewDashCard } from "metabase/lib/dashboard_grid";
 
-import { fetchDatabaseMetadata } from "metabase/redux/metadata";
+import { addParamValues, fetchDatabaseMetadata } from "metabase/redux/metadata";
+
 
 import { DashboardApi, MetabaseApi, CardApi, RevisionApi, PublicApi, EmbedApi } from "metabase/services";
 
@@ -45,11 +47,7 @@ export const DELETE_CARD = "metabase/dashboard/DELETE_CARD";
 
 // NOTE: this is used in metabase/redux/metadata but can't be imported directly due to circular reference
 export const FETCH_DASHBOARD = "metabase/dashboard/FETCH_DASHBOARD";
-export const FETCH_DASHBOARDS = "metabase/dashboard/FETCH_DASHBOARDS";
-export const CREATE_DASHBOARD = "metabase/dashboard/CREATE_DASHBOARD";
-export const SAVE_DASHBOARD = "metabase/dashboard/SAVE_DASHBOARD";
-export const UPDATE_DASHBOARD = "metabase/dashboard/UPDATE_DASHBOARD";
-export const DELETE_DASHBOARD = "metabase/dashboard/DELETE_DASHBOARD";
+export const SAVE_DASHBOARD_AND_CARDS = "metabase/dashboard/SAVE_DASHBOARD_AND_CARDS";
 export const SET_DASHBOARD_ATTRIBUTES = "metabase/dashboard/SET_DASHBOARD_ATTRIBUTES";
 
 export const ADD_CARD_TO_DASH = "metabase/dashboard/ADD_CARD_TO_DASH";
@@ -122,7 +120,7 @@ export const deleteCard = createThunkAction(DELETE_CARD, function(cardId) {
 export const addCardToDashboard = function({ dashId, cardId }: { dashId: DashCardId, cardId: CardId }) {
     return function(dispatch, getState) {
         const { dashboards, dashcards, cards } = getState().dashboard;
-        const dashboard: Dashboard = dashboards[dashId];
+        const dashboard: DashboardWithCards = dashboards[dashId];
         const existingCards: Array<DashCard> = dashboard.ordered_cards.map(id => dashcards[id]).filter(dc => !dc.isRemoved);
         const card: Card = cards[cardId];
         const dashcard: DashCard = {
@@ -140,6 +138,80 @@ export const addCardToDashboard = function({ dashId, cardId }: { dashId: DashCar
     };
 }
 
+export const saveDashboardAndCards = createThunkAction(SAVE_DASHBOARD_AND_CARDS, function() {
+    return async function (dispatch, getState) {
+        let {dashboards, dashcards, dashboardId} = getState().dashboard;
+        let dashboard = {
+            ...dashboards[dashboardId],
+            ordered_cards: dashboards[dashboardId].ordered_cards.map(dashcardId => dashcards[dashcardId])
+        };
+
+        // remove isRemoved dashboards
+        await Promise.all(dashboard.ordered_cards
+            .filter(dc => dc.isRemoved && !dc.isAdded)
+            .map(dc => DashboardApi.removecard({ dashId: dashboard.id, dashcardId: dc.id })));
+
+        // add isAdded dashboards
+        let updatedDashcards = await Promise.all(dashboard.ordered_cards
+            .filter(dc => !dc.isRemoved)
+            .map(async dc => {
+                if (dc.isAdded) {
+                    let result = await DashboardApi.addcard({ dashId: dashboard.id, cardId: dc.card_id });
+                    dispatch(updateDashcardId(dc.id, result.id));
+
+                    // mark isAdded because addcard doesn't record the position
+                    return {
+                        ...result,
+                        col: dc.col, row: dc.row,
+                        sizeX: dc.sizeX, sizeY: dc.sizeY,
+                        series: dc.series,
+                        parameter_mappings: dc.parameter_mappings,
+                        visualization_settings: dc.visualization_settings,
+                        isAdded: true
+                    }
+                } else {
+                    return dc;
+                }
+            }));
+
+        // update modified cards
+        await Promise.all(dashboard.ordered_cards
+            .filter(dc => dc.card.isDirty)
+            .map(async dc => CardApi.update(dc.card)));
+
+        // update the dashboard itself
+        if (dashboard.isDirty) {
+            let { id, name, description, parameters } = dashboard
+            await dispatch(saveDashboard({ id, name, description, parameters }));
+        }
+
+        // reposition the cards
+        if (_.some(updatedDashcards, (dc) => dc.isDirty || dc.isAdded)) {
+            let cards = updatedDashcards.map(({ id, card_id, row, col, sizeX, sizeY, series, parameter_mappings, visualization_settings }) =>
+                ({
+                    id, card_id, row, col, sizeX, sizeY, series, visualization_settings,
+                    parameter_mappings: parameter_mappings && parameter_mappings.filter(mapping =>
+                            // filter out mappings for deleted paramters
+                        _.findWhere(dashboard.parameters, { id: mapping.parameter_id }) &&
+                        // filter out mappings for deleted series
+                        (card_id === mapping.card_id || _.findWhere(series, { id: mapping.card_id }))
+                    )
+                })
+            );
+
+            const result = await DashboardApi.reposition_cards({ dashId: dashboard.id, cards });
+            if (result.status !== "ok") {
+                throw new Error(result.status);
+            }
+        }
+
+        await dispatch(saveDashboard(dashboard));
+
+        // make sure that we've fully cleared out any dirty state from editing (this is overkill, but simple)
+        dispatch(fetchDashboard(dashboard.id, null, true)); // disable using query parameters when saving
+    }
+});
+
 export const removeCardFromDashboard = createAction(REMOVE_CARD_FROM_DASH);
 
 const updateDashcardId = createAction(UPDATE_DASHCARD_ID, (oldDashcardId, newDashcardId) => ({ oldDashcardId, newDashcardId }));
@@ -307,6 +379,10 @@ export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(dashId
                 .each((dbId) => dispatch(fetchDatabaseMetadata(dbId)));
         }
 
+        if (dashboard.param_values) {
+            dispatch(addParamValues(dashboard.param_values));
+        }
+
         return {
             ...normalize(result, dashboard), // includes `result` and `entities`
             dashboardId: dashId,
@@ -325,125 +401,6 @@ export const updateEmbeddingParams = createAction(UPDATE_EMBEDDING_PARAMS, ({ id
     DashboardApi.update({ id, embedding_params })
 );
 
-export const saveDashboard = createThunkAction(SAVE_DASHBOARD, function(dashId) {
-    return async function(dispatch, getState) {
-        let { dashboards, dashcards, dashboardId } = getState().dashboard;
-        let dashboard = {
-            ...dashboards[dashboardId],
-            ordered_cards: dashboards[dashboardId].ordered_cards.map(dashcardId => dashcards[dashcardId])
-        };
-
-        // remove isRemoved dashboards
-        await Promise.all(dashboard.ordered_cards
-            .filter(dc => dc.isRemoved && !dc.isAdded)
-            .map(dc => DashboardApi.removecard({ dashId: dashboard.id, dashcardId: dc.id })));
-
-        // add isAdded dashboards
-        let updatedDashcards = await Promise.all(dashboard.ordered_cards
-            .filter(dc => !dc.isRemoved)
-            .map(async dc => {
-                if (dc.isAdded) {
-                    let result = await DashboardApi.addcard({ dashId, cardId: dc.card_id });
-                    dispatch(updateDashcardId(dc.id, result.id));
-                    // mark isAdded because addcard doesn't record the position
-                    return {
-                        ...result,
-                        col: dc.col, row: dc.row,
-                        sizeX: dc.sizeX, sizeY: dc.sizeY,
-                        series: dc.series,
-                        parameter_mappings: dc.parameter_mappings,
-                        visualization_settings: dc.visualization_settings,
-                        isAdded: true
-                    }
-                } else {
-                    return dc;
-                }
-            }));
-
-        // update modified cards
-        await Promise.all(dashboard.ordered_cards
-            .filter(dc => dc.card.isDirty)
-            .map(async dc => CardApi.update(dc.card)));
-
-        // update the dashboard itself
-        if (dashboard.isDirty) {
-            let { id, name, description, parameters } = dashboard;
-            dashboard = await DashboardApi.update({ id, name, description, parameters });
-        }
-
-        // reposition the cards
-        if (_.some(updatedDashcards, (dc) => dc.isDirty || dc.isAdded)) {
-            let cards = updatedDashcards.map(({ id, card_id, row, col, sizeX, sizeY, series, parameter_mappings, visualization_settings }) =>
-                ({
-                    id, card_id, row, col, sizeX, sizeY, series, visualization_settings,
-                    parameter_mappings: parameter_mappings && parameter_mappings.filter(mapping =>
-                        // filter out mappings for deleted paramters
-                        _.findWhere(dashboard.parameters, { id: mapping.parameter_id }) &&
-                        // filter out mappings for deleted series
-                        (card_id === mapping.card_id || _.findWhere(series, { id: mapping.card_id }))
-                    )
-                })
-            );
-            var result = await DashboardApi.reposition_cards({ dashId, cards });
-            if (result.status !== "ok") {
-                throw new Error(result.status);
-            }
-        }
-
-        // make sure that we've fully cleared out any dirty state from editing (this is overkill, but simple)
-        dispatch(fetchDashboard(dashId, null, true)); // disable using query parameters when saving
-
-        MetabaseAnalytics.trackEvent("Dashboard", "Update");
-
-        return dashboard;
-    };
-});
-
-export const updateDashboard = createThunkAction(UPDATE_DASHBOARD, (dashboard) =>
-    async (dispatch, getState) => {
-        const {
-            id,
-            name,
-            description,
-            parameters,
-            caveats,
-            points_of_interest,
-            show_in_getting_started
-        } = dashboard;
-
-        const cleanDashboard = {
-            id,
-            name,
-            description,
-            parameters,
-            caveats,
-            points_of_interest,
-            show_in_getting_started
-        };
-
-        const updatedDashboard = await DashboardApi.update(cleanDashboard);
-
-        MetabaseAnalytics.trackEvent("Dashboard", "Update");
-
-        return updatedDashboard;
-    }
-);
-
-export const fetchDashboards = createAction(FETCH_DASHBOARDS, () =>
-    DashboardApi.list({ f: "all" })
-);
-
-export const createDashboard = createAction(CREATE_DASHBOARD, (newDashboard) => {
-    MetabaseAnalytics.trackEvent("Dashboard", "Create");
-    return DashboardApi.create(newDashboard);
-});
-
-export const deleteDashboard = createAction(DELETE_DASHBOARD, async (dashId) => {
-    MetabaseAnalytics.trackEvent("Dashboard", "Delete");
-    await DashboardApi.delete({ dashId });
-    return dashId;
-});
-
 export const fetchRevisions = createThunkAction(FETCH_REVISIONS, function({ entity, id }) {
     return async function(dispatch, getState) {
         let revisions = await RevisionApi.list({ entity, id });
@@ -665,14 +622,6 @@ const parameterValues = handleActions({
     [FETCH_DASHBOARD]: { next: (state, { payload: { parameterValues }}) => parameterValues },
 }, {});
 
-const dashboardListing = handleActions({
-    [FETCH_DASHBOARDS]: (state, { payload }) => payload,
-    [CREATE_DASHBOARD]: (state, { payload }) => state.concat(payload),
-    [DELETE_DASHBOARD]: (state, { payload }) => state.filter(d => d.id !== payload),
-    [SAVE_DASHBOARD]:   (state, { payload }) => state.map(d => d.id === payload.id ? payload : d),
-    [UPDATE_DASHBOARD]: (state, { payload }) => state.map(d => d.id === payload.id ? payload : d),
-}, []);
-
 export default combineReducers({
     dashboardId,
     isEditing,
@@ -684,6 +633,5 @@ export default combineReducers({
     revisions,
     dashcardData,
     cardDurations,
-    parameterValues,
-    dashboardListing
+    parameterValues
 });
diff --git a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..7d8225ed669012a7467372328dc74512c5c094d0
--- /dev/null
+++ b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
@@ -0,0 +1,204 @@
+/* @flow */
+
+import React, { Component } from "react";
+
+import { connect } from "react-redux";
+import { replace } from "react-router-redux";
+
+import MetabaseAnalytics from "metabase/lib/analytics";
+import { parseHashOptions, stringifyHashOptions } from "metabase/lib/browser";
+
+import screenfull from "screenfull";
+
+import type { LocationDescriptor } from "metabase/meta/types";
+
+type Props = {
+    dashboardId:            string,
+    fetchDashboard:         (dashboardId: string) => Promise<any>;
+    fetchDashboardCardData: () => void;
+
+    location:               LocationDescriptor,
+    replace:                (location: LocationDescriptor) => void,
+};
+
+type State = {
+    isFullscreen: boolean,
+    isNightMode: boolean,
+    refreshPeriod: ?number,
+    refreshElapsed: ?number
+};
+
+const TICK_PERIOD = 0.25; // seconds
+
+/* This contains some state for dashboard controls on both private and embedded dashboards.
+ * It should probably be in Redux?
+ */
+export default (ComposedComponent: ReactClass<any>) =>
+    connect(null, { replace })(
+        class extends Component<*, Props, State> {
+            static displayName = "DashboardControls["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+
+            state = {
+                isFullscreen: false,
+                isNightMode: false,
+
+                refreshPeriod: null,
+                refreshElapsed: null
+            };
+
+            _interval: ?number;
+
+            componentWillMount() {
+                if (screenfull.enabled) {
+                    document.addEventListener(
+                        screenfull.raw.fullscreenchange,
+                        this._fullScreenChanged
+                    );
+                }
+                this.loadDashboardParams();
+            }
+
+            componentDidUpdate() {
+                this.updateDashboardParams();
+                this._showNav(!this.state.isFullscreen);
+            }
+
+            componentWillUnmount() {
+                this._showNav(true);
+                this._clearRefreshInterval();
+                if (screenfull.enabled) {
+                    document.removeEventListener(
+                        screenfull.raw.fullscreenchange,
+                        this._fullScreenChanged
+                    );
+                }
+            }
+
+            loadDashboardParams = () => {
+                let options = parseHashOptions(window.location.hash);
+                this.setRefreshPeriod(
+                    Number.isNaN(options.refresh) || options.refresh === 0
+                        ? null
+                        : options.refresh
+                );
+                this.setNightMode(options.theme === "night" || options.night); // DEPRECATED: options.night
+                this.setFullscreen(options.fullscreen);
+            };
+
+            updateDashboardParams = () => {
+                let options = parseHashOptions(window.location.hash);
+                const setValue = (name, value) => {
+                    if (value) {
+                        options[name] = value;
+                    } else {
+                        delete options[name];
+                    }
+                };
+                setValue("refresh", this.state.refreshPeriod);
+                setValue("fullscreen", this.state.isFullscreen);
+                setValue("theme", this.state.isNightMode ? "night" : null);
+                delete options.night; // DEPRECATED: options.night
+
+                let hash = stringifyHashOptions(options);
+                hash = hash ? "#" + hash : "";
+
+                const { location, replace } = this.props;
+                if (hash !== location.hash) {
+                    replace({
+                        pathname: location.pathname,
+                        earch: location.search,
+                        hash
+                    });
+                }
+            };
+
+            setRefreshPeriod = refreshPeriod => {
+                this._clearRefreshInterval();
+                if (refreshPeriod != null) {
+                    this._interval = setInterval(
+                        this._tickRefreshClock,
+                        TICK_PERIOD * 1000
+                    );
+                    this.setState({ refreshPeriod, refreshElapsed: 0 });
+                    MetabaseAnalytics.trackEvent(
+                        "Dashboard",
+                        "Set Refresh",
+                        refreshPeriod
+                    );
+                } else {
+                    this.setState({
+                        refreshPeriod: null,
+                        refreshElapsed: null
+                    });
+                }
+            };
+
+            setNightMode = isNightMode => {
+                this.setState({ isNightMode });
+            };
+
+            setFullscreen = (isFullscreen, browserFullscreen = true) => {
+                if (isFullscreen !== this.state.isFullscreen) {
+                    if (screenfull.enabled && browserFullscreen) {
+                        if (isFullscreen) {
+                            screenfull.request();
+                        } else {
+                            screenfull.exit();
+                        }
+                    }
+                    this.setState({ isFullscreen });
+                }
+            };
+
+            _tickRefreshClock = async () => {
+                let refreshElapsed = (this.state.refreshElapsed || 0) +
+                    TICK_PERIOD;
+                if (this.state.refreshPeriod && refreshElapsed >= this.state.refreshPeriod) {
+                    this.setState({ refreshElapsed: 0 });
+                    await this.props.fetchDashboard(
+                        this.props.dashboardId,
+                        this.props.location.query
+                    );
+                    this.props.fetchDashboardCardData({
+                        reload: true,
+                        clear: false
+                    });
+                } else {
+                    this.setState({ refreshElapsed });
+                }
+            };
+
+            _clearRefreshInterval() {
+                if (this._interval != null) {
+                    clearInterval(this._interval);
+                }
+            }
+
+            _showNav(show) {
+                const nav = document.querySelector(".Nav");
+                if (show && nav) {
+                    nav.classList.remove("hide");
+                } else if (!show && nav) {
+                    nav.classList.add("hide");
+                }
+            }
+
+            _fullScreenChanged = () => {
+                this.setState({ isFullscreen: screenfull.isFullscreen });
+            };
+
+            render() {
+                return (
+                    <ComposedComponent
+                        {...this.props}
+                        {...this.state}
+                        loadDashboardParams={this.loadDashboardParams}
+                        updateDashboardParams={this.updateDashboardParams}
+                        onNightModeChange={this.setNightMode}
+                        onFullscreenChange={this.setFullscreen}
+                        onRefreshPeriodChange={this.setRefreshPeriod}
+                    />
+                );
+            }
+        }
+    );
diff --git a/frontend/src/metabase/dashboard/selectors.js b/frontend/src/metabase/dashboard/selectors.js
index b486d60e1da6959c9d5c03bbeeb3f3cd533bf53b..171796da59ad083a4b28046beaa7a50e9b2949e3 100644
--- a/frontend/src/metabase/dashboard/selectors.js
+++ b/frontend/src/metabase/dashboard/selectors.js
@@ -6,10 +6,12 @@ import { updateIn, setIn } from "icepick";
 import { createSelector } from 'reselect';
 
 import * as Dashboard from "metabase/meta/Dashboard";
+import { getParameterTargetFieldId } from "metabase/meta/Parameter";
 import Metadata from "metabase/meta/metadata/Metadata";
 
 import type { CardId, Card } from "metabase/meta/types/Card";
-import type { ParameterId, DashCardId, ParameterMappingUIOption, Parameter, ParameterMapping } from "metabase/meta/types/Dashboard";
+import type { DashCardId } from "metabase/meta/types/Dashboard";
+import type { ParameterId, Parameter, ParameterMapping, ParameterMappingUIOption } from "metabase/meta/types/Parameter";
 
 export type AugmentedParameterMapping = ParameterMapping & {
     dashcard_id: DashCardId,
@@ -108,9 +110,10 @@ export const getMappingsByParameter = createSelector(
         for (const dashcard of dashboard.ordered_cards) {
             const cards: Array<Card> = [dashcard.card].concat(dashcard.series);
             for (let mapping: ParameterMapping of (dashcard.parameter_mappings || [])) {
-                let card = _.findWhere(cards, { id: mapping.card_id });
-                let field = card && card.dataset_query && Dashboard.getParameterMappingTargetField(metadata, card, mapping.target);
-                let values = field && field.values() || [];
+                const card = _.findWhere(cards, { id: mapping.card_id });
+                const fieldId = card && getParameterTargetFieldId(mapping.target, card.dataset_query);
+                const field = metadata.field(fieldId);
+                const values = field && field.values() || [];
                 for (const value of values) {
                     countsByParameter = updateIn(countsByParameter, [mapping.parameter_id, value], (count = 0) => count + 1)
                 }
@@ -119,6 +122,7 @@ export const getMappingsByParameter = createSelector(
                     parameter_id: mapping.parameter_id,
                     dashcard_id: dashcard.id,
                     card_id: mapping.card_id,
+                    field_id: fieldId,
                     values
                 };
                 mappingsByParameter = setIn(mappingsByParameter, [mapping.parameter_id, dashcard.id, mapping.card_id], augmentedMapping);
@@ -143,6 +147,25 @@ export const getMappingsByParameter = createSelector(
     }
 );
 
+/** Returns the dashboard's parameters objects, with field_id added, if appropriate */
+export const getParameters = createSelector(
+    [getDashboard, getMappingsByParameter],
+    (dashboard, mappingsByParameter) =>
+        (dashboard && dashboard.parameters || []).map(parameter => {
+            // get the unique list of field IDs these mappings reference
+            const fieldIds = _.chain(mappingsByParameter[parameter.id])
+                .map(_.values)
+                .flatten()
+                .map(m => m.field_id)
+                .uniq()
+                .value();
+            return {
+                ...parameter,
+                field_id: fieldIds.length === 1 ? fieldIds[0] : null
+            }
+        })
+)
+
 export const makeGetParameterMappingOptions = () => {
     const getParameterMappingOptions = createSelector(
         [getMetadata, getEditingParameter, getCard],
diff --git a/frontend/src/metabase/dashboards/components/DashboardList.jsx b/frontend/src/metabase/dashboards/components/DashboardList.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..50ed2de2e26163931b5e601628ceeb1f12ab6dde
--- /dev/null
+++ b/frontend/src/metabase/dashboards/components/DashboardList.jsx
@@ -0,0 +1,70 @@
+/* @flow */
+
+import React, {Component} from "react";
+import PropTypes from "prop-types";
+import {Link} from "react-router";
+import {withState} from "recompose";
+import cx from "classnames";
+import moment from "moment";
+
+import * as Urls from "metabase/lib/urls";
+
+import type {Dashboard} from "metabase/meta/types/Dashboard";
+import Icon from "metabase/components/Icon";
+import Ellipsified from "metabase/components/Ellipsified.jsx";
+
+type DashboardListItemType = {
+    dashboard: Dashboard,
+    hover: boolean,
+    setHover: (boolean) => void
+}
+
+const enhance = withState('hover', 'setHover', false)
+const DashboardListItem = enhance(({dashboard, hover, setHover}: DashboardListItemType) =>
+    <li className="Grid-cell shrink-below-content-size" style={{maxWidth: "550px"}}>
+        <Link to={Urls.dashboard(dashboard.id)}
+              data-metabase-event={"Navbar;Dashboards;Open Dashboard;" + dashboard.id}
+              className={cx(
+                  "flex align-center border-box p2 rounded no-decoration transition-background",
+                  {"bg-white": !hover},
+                  {"bg-brand": hover}
+              )}
+              style={{
+                  border: "1px solid rgba(220,225,228,0.50)",
+                  boxShadow: "0 1px 3px 0 rgba(220,220,220,0.50)",
+                  height: "80px"
+              }}
+              onMouseEnter={() => setHover(true)}
+              onMouseLeave={() => setHover(false)}>
+            <Icon name="dashboard"
+                  className={cx("pr2", {"text-grey-1": !hover}, {"text-brand-darken": hover})} size={32}/>
+            <div className={cx("flex-full shrink-below-content-size")}>
+                <h4 className={cx("text-ellipsis text-nowrap overflow-hidden text-brand", {"text-white": hover})}
+                    style={{marginBottom: "0.2em"}}>
+                    <Ellipsified>{dashboard.name}</Ellipsified>
+                </h4>
+                <div
+                    className={cx("text-smaller text-uppercase text-bold", {"text-grey-3": !hover}, {"text-grey-2": hover})}>
+                    {/* NOTE: Could these time formats be centrally stored somewhere? */}
+                    {moment(dashboard.created_at).format('MMM D, YYYY')}
+                </div>
+            </div>
+        </Link>
+    </li>
+);
+
+export default class DashboardList extends Component {
+    static propTypes = {
+        dashboards: PropTypes.array.isRequired
+    };
+
+    render() {
+        const {dashboards} = this.props;
+
+        return (
+            <ol className="Grid Grid--guttersXl Grid--full small-Grid--1of2 md-Grid--1of3">
+                { dashboards.map(dash => <DashboardListItem key={dash.id} dashboard={dash}/>)}
+            </ol>
+        );
+    }
+}
diff --git a/frontend/src/metabase/dashboards/containers/Dashboards.jsx b/frontend/src/metabase/dashboards/containers/Dashboards.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..becdad46d02ad4554a6c38423772c53ffb6e6feb
--- /dev/null
+++ b/frontend/src/metabase/dashboards/containers/Dashboards.jsx
@@ -0,0 +1,154 @@
+/* @flow */
+
+import React, {Component, PropTypes} from 'react';
+import {connect} from "react-redux";
+import cx from "classnames";
+
+import type {Dashboard} from "metabase/meta/types/Dashboard";
+
+import DashboardList from "../components/DashboardList";
+
+import TitleAndDescription from "metabase/components/TitleAndDescription";
+import CreateDashboardModal from "metabase/components/CreateDashboardModal";
+import Modal from "metabase/components/Modal.jsx";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import Icon from "metabase/components/Icon.jsx";
+import SearchHeader from "metabase/components/SearchHeader";
+import EmptyState from "metabase/components/EmptyState";
+
+import {caseInsensitiveSearch} from "metabase/lib/string"
+
+import * as dashboardsActions from "../dashboards";
+import {getDashboardListing} from "../selectors";
+
+
+const mapStateToProps = (state, props) => ({
+    dashboards: getDashboardListing(state)
+});
+
+const mapDispatchToProps = dashboardsActions;
+
+export class Dashboards extends Component {
+    props: {
+        dashboards: Dashboard[],
+        createDashboard: (Dashboard) => any,
+        fetchDashboards: PropTypes.func.isRequired,
+    };
+
+    state = {
+        modalOpen: false,
+        searchText: ""
+    }
+
+    componentWillMount() {
+        this.props.fetchDashboards();
+    }
+
+    async onCreateDashboard(newDashboard: Dashboard) {
+        let {createDashboard} = this.props;
+
+        try {
+            await createDashboard(newDashboard, {redirect: true});
+        } catch (e) {
+            console.log("createDashboard failed", e);
+        }
+    }
+
+    showCreateDashboard = () => {
+        this.setState({modalOpen: !this.state.modalOpen});
+    }
+
+    hideCreateDashboard = () => {
+        this.setState({modalOpen: false});
+    }
+
+    renderCreateDashboardModal() {
+        return (
+            <Modal>
+                <CreateDashboardModal
+                    createDashboardFn={this.onCreateDashboard.bind(this)}
+                    onClose={this.hideCreateDashboard}/>
+            </Modal>
+        );
+    }
+
+    getFilteredDashboards = () => {
+        const {searchText} = this.state;
+        const {dashboards} = this.props;
+
+        if (searchText === "") {
+            return dashboards;
+        } else {
+            return dashboards.filter(({name, description}) =>
+                caseInsensitiveSearch(name,searchText) || (description && caseInsensitiveSearch(description, searchText))
+            );
+        }
+    }
+
+    render() {
+        let {modalOpen, searchText} = this.state;
+
+        const isLoading = this.props.dashboards === null
+        const noDashboardsCreated = this.props.dashboards && this.props.dashboards.length === 0
+        const filteredDashboards = isLoading ? [] : this.getFilteredDashboards();
+        const noSearchResults = searchText !== "" && filteredDashboards.length === 0;
+
+        return (
+            <LoadingAndErrorWrapper
+                loading={isLoading}
+                className={cx("relative mx4", {"flex-full flex align-center justify-center": noDashboardsCreated})}
+            >
+                { modalOpen ? this.renderCreateDashboardModal() : null }
+                { noDashboardsCreated ?
+                    <div className="mt2">
+                        <EmptyState
+                            message={<span>Put the charts and graphs you look at <br/>frequently in a single, handy place.</span>}
+                            image="/app/img/dashboard_illustration"
+                            action="Create a dashboard"
+                            onActionClick={this.showCreateDashboard}
+                            className="mt2"
+                            imageClassName="mln2"
+                        />
+                    </div>
+                    : <div>
+                        <div className="flex align-center pt4 pb1">
+                            <TitleAndDescription title="Dashboards"/>
+                            <div className="flex-align-right cursor-pointer text-grey-5 text-brand-hover">
+                                <Icon name="add"
+                                      size={20}
+                                      onClick={this.showCreateDashboard}/>
+                            </div>
+                        </div>
+                        <div className="flex align-center pb1">
+                            <SearchHeader
+                                searchText={searchText}
+                                setSearchText={(text) => this.setState({searchText: text})}
+                            />
+                        </div>
+                        { noSearchResults ?
+                            <div className="flex justify-center">
+                                <EmptyState
+                                    message={
+                                        <div className="mt4">
+                                            <h3 className="text-grey-5">No results found</h3>
+                                            <p className="text-grey-4">Try adjusting your filter to find what you’re
+                                                looking for.</p>
+                                        </div>
+                                    }
+                                    image="/app/img/empty_dashboard"
+                                    action="Create a dashboard"
+                                    imageClassName="mln2"
+                                    smallDescription
+                                />
+                            </div>
+                            : <DashboardList dashboards={filteredDashboards}/>
+                        }
+                    </div>
+
+                }
+            </LoadingAndErrorWrapper>
+        );
+    }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboards)
diff --git a/frontend/src/metabase/dashboards/dashboards.js b/frontend/src/metabase/dashboards/dashboards.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd8bf38dc8da18130df5a2bd0fe65aefb3d0760c
--- /dev/null
+++ b/frontend/src/metabase/dashboards/dashboards.js
@@ -0,0 +1,106 @@
+/* @flow weak */
+
+import { handleActions, createAction, combineReducers, createThunkAction } from "metabase/lib/redux";
+import { DashboardApi } from "metabase/services";
+import MetabaseAnalytics from "metabase/lib/analytics";
+import moment from 'moment';
+
+import * as Urls from "metabase/lib/urls";
+import { push } from "react-router-redux";
+import type { Dashboard } from "metabase/meta/types/Dashboard";
+
+export const FETCH_DASHBOARDS = "metabase/dashboards/FETCH_DASHBOARDS";
+export const CREATE_DASHBOARD = "metabase/dashboards/CREATE_DASHBOARD";
+export const DELETE_DASHBOARD = "metabase/dashboards/DELETE_DASHBOARD";
+export const SAVE_DASHBOARD = "metabase/dashboards/SAVE_DASHBOARD";
+export const UPDATE_DASHBOARD = "metabase/dashboards/UPDATE_DASHBOARD";
+
+/**
+ * Actions that retrieve/update the basic information of dashboards
+ * `dashboards.dashboardListing` holds an array of all dashboards without cards
+ */
+
+export const fetchDashboards = createThunkAction(FETCH_DASHBOARDS, () =>
+    async function(dispatch, getState) {
+        const dashboards = await DashboardApi.list({f: "all"})
+
+        for (const dashboard of dashboards) {
+            dashboard.updated_at = moment(dashboard.updated_at);
+        }
+
+        return dashboards;
+    }
+);
+
+type CreateDashboardOpts = {
+    redirect?: boolean
+}
+export const createDashboard = createThunkAction(CREATE_DASHBOARD, (dashboard: Dashboard, { redirect }: CreateDashboardOpts) =>
+    async (dispatch, getState) => {
+        MetabaseAnalytics.trackEvent("Dashboard", "Create");
+        const createdDashboard: Dashboard = await DashboardApi.create(dashboard);
+
+        if (redirect) {
+            dispatch(push(Urls.dashboard(createdDashboard.id)));
+        }
+
+        return createdDashboard;
+    }
+);
+
+export const updateDashboard = createThunkAction(UPDATE_DASHBOARD, (dashboard: Dashboard) =>
+    async (dispatch, getState) => {
+        const {
+            id,
+            name,
+            description,
+            parameters,
+            caveats,
+            points_of_interest,
+            show_in_getting_started
+        } = dashboard;
+
+        const cleanDashboard = {
+            id,
+            name,
+            description,
+            parameters,
+            caveats,
+            points_of_interest,
+            show_in_getting_started
+        };
+
+        const updatedDashboard = await DashboardApi.update(cleanDashboard);
+
+        MetabaseAnalytics.trackEvent("Dashboard", "Update");
+
+        return updatedDashboard;
+    }
+);
+
+export const deleteDashboard = createAction(DELETE_DASHBOARD, async (dashId) => {
+    MetabaseAnalytics.trackEvent("Dashboard", "Delete");
+    await DashboardApi.delete({ dashId });
+    return dashId;
+});
+
+export const saveDashboard = createThunkAction(SAVE_DASHBOARD, function(dashboard: Dashboard) {
+    return async function(dispatch, getState): Promise<Dashboard> {
+        let { id, name, description, parameters } = dashboard
+        MetabaseAnalytics.trackEvent("Dashboard", "Update");
+        return await DashboardApi.update({ id, name, description, parameters });
+    };
+});
+
+const dashboardListing = handleActions({
+    [FETCH_DASHBOARDS]: (state, { payload }) => payload,
+    [CREATE_DASHBOARD]: (state, { payload }) => (state || []).concat(payload),
+    [DELETE_DASHBOARD]: (state, { payload }) => (state || []).filter(d => d.id !== payload),
+    [SAVE_DASHBOARD]:   (state, { payload }) => (state || []).map(d => d.id === payload.id ? payload : d),
+    [UPDATE_DASHBOARD]: (state, { payload }) => (state || []).map(d => d.id === payload.id ? payload : d),
+}, null);
+
+export default combineReducers({
+    dashboardListing
+});
+
diff --git a/frontend/src/metabase/dashboards/selectors.js b/frontend/src/metabase/dashboards/selectors.js
new file mode 100644
index 0000000000000000000000000000000000000000..28affff03424873dfa7320760915614260921c5f
--- /dev/null
+++ b/frontend/src/metabase/dashboards/selectors.js
@@ -0,0 +1 @@
+export const getDashboardListing = (state) => state.dashboards.dashboardListing;
diff --git a/frontend/src/metabase/home/containers/HomepageApp.jsx b/frontend/src/metabase/home/containers/HomepageApp.jsx
index 1d1eab33686ea58bdb5db549a821f4e93b244e60..aa6a9ef663c325de03fee069957220ce7951c5e9 100644
--- a/frontend/src/metabase/home/containers/HomepageApp.jsx
+++ b/frontend/src/metabase/home/containers/HomepageApp.jsx
@@ -77,7 +77,7 @@ export default class HomepageApp extends Component {
                     </Modal>
                 : null }
 
-                <div className="CheckBg bg-brand text-white md-pl4">
+                <div className="bg-brand text-white md-pl4">
                     <div style={{marginRight: 346}}>
                         <div className="Layout-mainColumn">
                             <header style={this.styles.headerGreeting} className="flex align-center pb4 pt1">
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index c37829a1bf34736f4894d71675186dfd5477b151..b96cfc59f07c5c53af26bb6446675831cec22950 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -38,10 +38,6 @@ export var ICON_PATHS = {
     },
     close: 'M4 8 L8 4 L16 12 L24 4 L28 8 L20 16 L28 24 L24 28 L16 20 L8 28 L4 24 L12 16 z ',
     collection: 'M16.5695046,2.82779686 L15.5639388,2.83217072 L30.4703127,11.5065092 L30.4818076,9.80229623 L15.5754337,18.2115855 L16.5436335,18.2077098 L1.65289961,9.96407638 L1.67877073,11.6677911 L16.5695046,2.82779686 Z M0.691634577,11.6826271 L15.5823685,19.9262606 C15.8836872,20.0930731 16.2506087,20.0916044 16.5505684,19.9223849 L31.4569423,11.5130957 C32.1196316,11.1392458 32.1260238,10.1915465 31.4684372,9.80888276 L16.5620632,1.1345443 C16.2511162,0.953597567 15.8658421,0.955273376 15.5564974,1.13891816 L0.665763463,9.97891239 C0.0118284022,10.3671258 0.0262104889,11.3142428 0.691634577,11.6826271 Z M15.5699489,25.798061 L16.0547338,26.0652615 L16.536759,25.7931643 L31.4991818,17.3470627 C31.973977,17.0790467 32.1404815,16.4788587 31.8710802,16.0065052 C31.6016788,15.5341517 30.9983884,15.3685033 30.5235933,15.6365193 L15.5611705,24.0826209 L16.5279806,24.0777242 L1.46763754,15.7768642 C0.99012406,15.5136715 0.388560187,15.6854222 0.124007019,16.16048 C-0.14054615,16.6355379 0.0320922897,17.2340083 0.509605765,17.497201 L15.5699489,25.798061 Z M15.5699489,31.7327994 L16.0547338,32 L16.536759,31.7279028 L31.4991818,23.2818011 C31.973977,23.0137852 32.1404815,22.4135972 31.8710802,21.9412437 C31.6016788,21.4688901 30.9983884,21.3032418 30.5235933,21.5712578 L15.5611705,30.0173594 L16.5279806,30.0124627 L1.46763754,21.7116027 C0.99012406,21.44841 0.388560187,21.6201606 0.124007019,22.0952185 C-0.14054615,22.5702764 0.0320922897,23.1687467 0.509605765,23.4319394 L15.5699489,31.7327994 Z',
-    countrymap: {
-        path: 'M19.4375,6.97396734 L27,8.34417492 L27,25.5316749 L18.7837192,23.3765689 L11.875,25.3366259 L11.875,25.3366259 L11.875,11.0941749 L11.1875,11.0941749 L11.1875,25.5316749 L5,24.1566749 L5,7.65667492 L11.1875,9.03167492 L18.75,6.90135976 L18.75,22.0941749 L19.4375,22.0941749 L19.4375,6.97396734 Z',
-        attrs: { scale: 2 }
-    },
     compass_needle: {
         path: 'M0 32l10.706-21.064L32 0 21.22 20.89 0 32zm16.092-12.945a3.013 3.013 0 0 0 3.017-3.009 3.013 3.013 0 0 0-3.017-3.008 3.013 3.013 0 0 0-3.017 3.008 3.013 3.013 0 0 0 3.017 3.009z'
     },
@@ -168,10 +164,6 @@ export var ICON_PATHS = {
     segment: 'M2.99631547,14.0294075 L2.99631579,1.99517286 C2.99631582,0.893269315 3.89614282,0 4.98985651,0 L30.0064593,0 C31.1074614,0 32,0.895880847 32,2.00761243 L32,26.8688779 C32,27.9776516 31.1071386,28.8764903 30.0003242,28.8764903 L17.7266598,28.8764903 L17.7266594,14.0294075 L2.99631547,14.0294075 Z M-7.10651809e-15,16.9955967 L-7.10651809e-15,30.0075311 C-7.10651809e-15,31.1079413 0.900469916,32 2.00155906,32 L14.3949712,32 L14.3949712,16.9955967 L-7.10651809e-15,16.9955967 Z',
     star: 'M16 0 L21 11 L32 12 L23 19 L26 31 L16 25 L6 31 L9 19 L0 12 L11 11',
     staroutline: "M16 21.935l5.967 3.14-1.14-6.653 4.828-4.712-6.671-.97L16 6.685l-2.984 6.053-6.67.971 4.827 4.712-1.14 6.654L16 21.935zm-9.892 8.547l1.89-11.029L0 11.647l11.053-1.609L16 0l4.947 10.038L32 11.647l-7.997 7.806 1.889 11.03L16 25.274l-9.892 5.207z",
-    statemap: {
-        path: 'M19.4375,6.97396734 L27,8.34417492 L27,25.5316749 L18.7837192,23.3765689 L11.875,25.3366259 L11.875,25.3366259 L11.875,11.0941749 L11.1875,11.0941749 L11.1875,25.5316749 L5,24.1566749 L5,7.65667492 L11.1875,9.03167492 L18.75,6.90135976 L18.75,22.0941749 L19.4375,22.0941749 L19.4375,6.97396734 Z',
-        attrs: { scale: 2 }
-    },
     string: {
         path: 'M14.022,18 L11.533,18 C11.2543319,18 11.0247509,17.935084 10.84425,17.80525 C10.6637491,17.675416 10.538667,17.5091677 10.469,17.3065 L9.652,14.8935 L4.389,14.8935 L3.572,17.3065 C3.50866635,17.4838342 3.38516758,17.6437493 3.2015,17.78625 C3.01783241,17.9287507 2.79300133,18 2.527,18 L0.019,18 L5.377,4.1585 L8.664,4.1585 L14.022,18 Z M5.13,12.7085 L8.911,12.7085 L7.638,8.918 C7.55566626,8.67733213 7.45908389,8.3939183 7.34825,8.06775 C7.23741611,7.7415817 7.12816721,7.3885019 7.0205,7.0085 C6.91916616,7.39483527 6.8146672,7.75266502 6.707,8.082 C6.5993328,8.41133498 6.49800047,8.69633213 6.403,8.937 L5.13,12.7085 Z M21.945,18 C21.6663319,18 21.4557507,17.9620004 21.31325,17.886 C21.1707493,17.8099996 21.0520005,17.6516679 20.957,17.411 L20.748,16.8695 C20.5009988,17.078501 20.2635011,17.2621659 20.0355,17.4205 C19.8074989,17.5788341 19.5715846,17.7134161 19.32775,17.82425 C19.0839154,17.9350839 18.8242514,18.0174164 18.54875,18.07125 C18.2732486,18.1250836 17.9676683,18.152 17.632,18.152 C17.1823311,18.152 16.7738352,18.0934173 16.4065,17.97625 C16.0391648,17.8590827 15.7272513,17.6865011 15.47075,17.4585 C15.2142487,17.2304989 15.016334,16.947085 14.877,16.60825 C14.737666,16.269415 14.668,15.8783355 14.668,15.435 C14.668,15.0866649 14.7566658,14.7288352 14.934,14.3615 C15.1113342,13.9941648 15.4184978,13.6600848 15.8555,13.35925 C16.2925022,13.0584152 16.8814963,12.8066677 17.6225,12.604 C18.3635037,12.4013323 19.297661,12.2873335 20.425,12.262 L20.425,11.844 C20.425,11.2676638 20.3062512,10.8512513 20.06875,10.59475 C19.8312488,10.3382487 19.4940022,10.21 19.057,10.21 C18.7086649,10.21 18.4236678,10.2479996 18.202,10.324 C17.9803322,10.4000004 17.7824175,10.4854995 17.60825,10.5805 C17.4340825,10.6755005 17.2646675,10.7609996 17.1,10.837 C16.9353325,10.9130004 16.7390011,10.951 16.511,10.951 C16.3083323,10.951 16.1357507,10.9019172 15.99325,10.80375 C15.8507493,10.7055828 15.7383337,10.5836674 15.656,10.438 L15.124,9.5165 C15.7193363,8.99083071 16.3795797,8.59975128 17.10475,8.34325 C17.8299203,8.08674872 18.6073292,7.9585 19.437,7.9585 C20.0323363,7.9585 20.5690809,8.05508237 21.04725,8.24825 C21.5254191,8.44141763 21.9307483,8.71058161 22.26325,9.05575 C22.5957517,9.40091839 22.8506658,9.81099763 23.028,10.286 C23.2053342,10.7610024 23.294,11.2803305 23.294,11.844 L23.294,18 L21.945,18 Z M18.563,16.2045 C18.9430019,16.2045 19.2754986,16.1380007 19.5605,16.005 C19.8455014,15.8719993 20.1336652,15.6566682 20.425,15.359 L20.425,13.991 C19.8359971,14.0163335 19.3515019,14.0669996 18.9715,14.143 C18.5914981,14.2190004 18.2906678,14.3139994 18.069,14.428 C17.8473322,14.5420006 17.6937504,14.6718326 17.60825,14.8175 C17.5227496,14.9631674 17.48,15.1214991 17.48,15.2925 C17.48,15.6281683 17.5718324,15.8640827 17.7555,16.00025 C17.9391676,16.1364173 18.2083316,16.2045 18.563,16.2045 L18.563,16.2045 Z',
         attrs: { viewBox: '0 0 24 24'}
diff --git a/frontend/src/metabase/lib/browser.js b/frontend/src/metabase/lib/browser.js
new file mode 100644
index 0000000000000000000000000000000000000000..ffda40750c3acdd437d3ea747ab8754cb7884ec5
--- /dev/null
+++ b/frontend/src/metabase/lib/browser.js
@@ -0,0 +1,18 @@
+
+import querystring from "querystring";
+
+export function parseHashOptions(hash) {
+    let options = querystring.parse(hash.replace(/^#/, ""));
+    for (var name in options) {
+        if (options[name] === "") {
+            options[name] = true;
+        } else if (/^(true|false|-?\d+(\.\d+)?)$/.test(options[name])) {
+            options[name] = JSON.parse(options[name]);
+        }
+    }
+    return options;
+}
+
+export function stringifyHashOptions(options) {
+    return querystring.stringify(options).replace(/=true\b/g, "");
+}
diff --git a/frontend/src/metabase/lib/browser.spec.js b/frontend/src/metabase/lib/browser.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..eb1acd96820c93ae44ab7cbca26f533faedb6cec
--- /dev/null
+++ b/frontend/src/metabase/lib/browser.spec.js
@@ -0,0 +1,61 @@
+import { parseHashOptions, stringifyHashOptions } from "./browser";
+
+describe("browser", () => {
+    describe("parseHashOptions", () => {
+        it ("should parse with prepended '#'", () => {
+            expect(parseHashOptions("#foo=bar")).toEqual({ "foo": "bar" });
+        })
+        it ("should parse without prepended '#'", () => {
+            expect(parseHashOptions("foo=bar")).toEqual({ "foo": "bar" });
+        })
+        it ("should parse strings", () => {
+            expect(parseHashOptions("#foo=bar")).toEqual({ "foo": "bar" });
+        })
+        it ("should parse numbers", () => {
+            expect(parseHashOptions("#foo=123")).toEqual({ "foo": 123 });
+        })
+        it ("should parse negative numbers", () => {
+            expect(parseHashOptions("#foo=-123")).toEqual({ "foo": -123 });
+        })
+        it ("should parse base key as true", () => {
+            expect(parseHashOptions("#foo")).toEqual({ "foo": true });
+        })
+        it ("should parse true", () => {
+            expect(parseHashOptions("#foo=true")).toEqual({ "foo": true });
+        })
+        it ("should parse false", () => {
+            expect(parseHashOptions("#foo=false")).toEqual({ "foo": false });
+        })
+        it ("should parse all the things", () => {
+            expect(parseHashOptions("#foo1=bar&foo2=123&foo3&foo4=true&foo5=false")).toEqual({
+                "foo1": "bar",
+                "foo2": 123,
+                "foo3": true,
+                "foo4": true,
+                "foo5": false
+            });
+        })
+    })
+    describe("stringifyHashOptions", () => {
+        it ("should stringify strings", () => {
+            expect(stringifyHashOptions({ "foo": "bar" })).toEqual("foo=bar");
+        })
+        it ("should stringify numbers", () => {
+            expect(stringifyHashOptions({ "foo": 123 })).toEqual("foo=123");
+        })
+        it ("should stringify base key as true", () => {
+            expect(stringifyHashOptions({ "foo": true })).toEqual("foo");
+        })
+        it ("should stringify false", () => {
+            expect(stringifyHashOptions({ "foo": false })).toEqual("foo=false");
+        })
+        it ("should stringify all the things", () => {
+            expect(stringifyHashOptions({
+                "foo1": "bar",
+                "foo2": 123,
+                "foo3": true,
+                "foo4": false
+            })).toEqual("foo1=bar&foo2=123&foo3&foo4=false");
+        })
+    })
+})
diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js
index f6db24482f44966263512c399afdcf79bdefa09f..3aa487d2ec9f0526716e7979e7391922382c4e8d 100644
--- a/frontend/src/metabase/lib/formatting.js
+++ b/frontend/src/metabase/lib/formatting.js
@@ -103,18 +103,19 @@ export function formatTimeWithUnit(value, unit, options = {}) {
     }
 }
 
-const EMAIL_WHITELIST_REGEX = /.+@.+/;
+// https://github.com/angular/angular.js/blob/v1.6.3/src/ng/directive/input.js#L27
+const EMAIL_WHITELIST_REGEX = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
 
 export function formatEmail(value, { jsx } = {}) {
     if (jsx && EMAIL_WHITELIST_REGEX.test(value)) {
         return <ExternalLink href={"mailto:" + value}>{value}</ExternalLink>;
     } else {
-        value;
+        return value;
     }
 }
 
-// prevent `javascript:` etc URLs
-const URL_WHITELIST_REGEX = /^(https?|mailto):/;
+// based on https://github.com/angular/angular.js/blob/v1.6.3/src/ng/directive/input.js#L25
+const URL_WHITELIST_REGEX = /^(https?|mailto):\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
 
 export function formatUrl(value, { jsx } = {}) {
     if (jsx && URL_WHITELIST_REGEX.test(value)) {
@@ -124,6 +125,15 @@ export function formatUrl(value, { jsx } = {}) {
     }
 }
 
+// fallback for formatting a string without a column special_type
+function formatStringFallback(value, options = {}) {
+    value = formatUrl(value, options);
+    if (typeof value === 'string') {
+        value = formatEmail(value, options);
+    }
+    return value;
+}
+
 export function formatValue(value, options = {}) {
     let column = options.column;
     options = {
@@ -142,7 +152,7 @@ export function formatValue(value, options = {}) {
     } else if (isDate(column) || moment.isDate(value) || moment.isMoment(value) || moment(value, ["YYYY-MM-DD'T'HH:mm:ss.SSSZ"], true).isValid()) {
         return parseTimestamp(value, column && column.unit).format("LLLL");
     } else if (typeof value === "string") {
-        return value;
+        return formatStringFallback(value, options);
     } else if (typeof value === "number") {
         if (isCoordinate(column)) {
             return DECIMAL_DEGREES_FORMATTER(value);
diff --git a/frontend/src/metabase/lib/query/field.js b/frontend/src/metabase/lib/query/field.js
index 85fc1d1885ed423adfc813f0e678cfb81b83e9f6..d4c928ff3c86082ca8db30d6ea830c9318460a25 100644
--- a/frontend/src/metabase/lib/query/field.js
+++ b/frontend/src/metabase/lib/query/field.js
@@ -1,12 +1,13 @@
 
-
 import { mbqlEq } from "./util";
 
-import type { Field } from "metabase/meta/types/Query";
+import type { Field as FieldReference } from "metabase/meta/types/Query";
+import type { Field, FieldId } from "metabase/meta/types/Field";
 
 // gets the target field ID (recursively) from any type of field, including raw field ID, fk->, and datetime-field cast.
-export function getFieldTargetId(field: Field): ?FieldId {
+export function getFieldTargetId(field: FieldReference): ?FieldId {
     if (isRegularField(field)) {
+        // $FlowFixMe
         return field;
     } else if (isLocalField(field)) {
         // $FlowFixMe
@@ -21,26 +22,39 @@ export function getFieldTargetId(field: Field): ?FieldId {
     console.warn("Unknown field type: ", field);
 }
 
-export function isRegularField(field: Field): boolean {
+export function isRegularField(field: FieldReference): boolean {
     return typeof field === "number";
 }
 
-export function isLocalField(field: Field): boolean {
+export function isLocalField(field: FieldReference): boolean {
     return Array.isArray(field) && mbqlEq(field[0], "field-id");
 }
 
-export function isForeignKeyField(field: Field): boolean {
+export function isForeignKeyField(field: FieldReference): boolean {
     return Array.isArray(field) && mbqlEq(field[0], "fk->");
 }
 
-export function isDatetimeField(field: Field): boolean {
+export function isDatetimeField(field: FieldReference): boolean {
     return Array.isArray(field) && mbqlEq(field[0], "datetime-field");
 }
 
-export function isExpressionField(field: Field): boolean {
+export function isExpressionField(field: FieldReference): boolean {
     return Array.isArray(field) && field.length === 2 && mbqlEq(field[0], "expression");
 }
 
-export function isAggregateField(field: Field): boolean {
+export function isAggregateField(field: FieldReference): boolean {
     return Array.isArray(field) && mbqlEq(field[0], "aggregation");
 }
+
+// Metadata field "values" type is inconsistent
+// https://github.com/metabase/metabase/issues/3417
+export function getFieldValues(field: ?Field): any[] {
+    const values = field && field.values;
+    if (Array.isArray(values)) {
+        return values;
+    } else if (values && Array.isArray(values.values)) {
+        return values.values;
+    } else {
+        return [];
+    }
+}
diff --git a/frontend/src/metabase/lib/redux.js b/frontend/src/metabase/lib/redux.js
index 9f2523301585ab8b60fbf059b699d7bcb69a7825..c1259080e302c6e871fc9b1eedef5f3d89e4b62f 100644
--- a/frontend/src/metabase/lib/redux.js
+++ b/frontend/src/metabase/lib/redux.js
@@ -16,7 +16,10 @@ export function createThunkAction(actionType, actionThunkCreator) {
         return async function(dispatch, getState) {
             try {
                 let payload = await thunk(dispatch, getState);
-                dispatch({ type: actionType, payload });
+                let dispatchValue = { type: actionType, payload };
+                dispatch(dispatchValue);
+
+                return dispatchValue;
             } catch (error) {
                 dispatch({ type: actionType, payload: error, error: true });
                 throw error;
diff --git a/frontend/src/metabase/lib/string.js b/frontend/src/metabase/lib/string.js
index 188e46b634616b22f7bc08328fcfd2855d466454..5fc78bed7c0d03ef9b8ef507ffb250accfc4c7cc 100644
--- a/frontend/src/metabase/lib/string.js
+++ b/frontend/src/metabase/lib/string.js
@@ -12,3 +12,7 @@ export function createMultiwordSearchRegex(input) {
 }
 
 export const countLines = (str) => str.split(/\n/g).length
+
+export function caseInsensitiveSearch(haystack, needle) {
+    return !needle || (haystack != null && haystack.toLowerCase().indexOf(needle.toLowerCase()) >= 0);
+}
diff --git a/frontend/src/metabase/meta/Card.js b/frontend/src/metabase/meta/Card.js
index 996ae2972b7e5ae3d108b6dbe593f85603f8a99d..9032a759139b68b214f136781a44daf89e9ce383 100644
--- a/frontend/src/metabase/meta/Card.js
+++ b/frontend/src/metabase/meta/Card.js
@@ -2,8 +2,11 @@
 
 import type { StructuredQuery, NativeQuery, TemplateTag } from "./types/Query";
 import type { Card, DatasetQuery, StructuredDatasetQuery, NativeDatasetQuery } from "./types/Card";
-import type { Parameter, ParameterId, ParameterMapping } from "metabase/meta/types/Dashboard";
-import { getTemplateTagParameters } from "metabase/meta/Parameter";
+import type { Parameter, ParameterMapping, ParameterValues } from "./types/Parameter";
+
+import { getTemplateTagParameters, getParameterTargetFieldId } from "metabase/meta/Parameter";
+
+import { assoc } from "icepick";
 
 declare class Object {
     static values<T>(object: { [key:string]: T }): Array<T>;
@@ -76,10 +79,25 @@ export function getParameters(card: ?Card): Parameter[] {
     return getTemplateTagParameters(tags);
 }
 
+export function getParametersWithExtras(card: Card, parameterValues?: ParameterValues) {
+    return getParameters(card).map(parameter => {
+        // if we have a parameter value for this parameter, set "value"
+        if (parameterValues && parameter.id in parameterValues) {
+            parameter = assoc(parameter, "value", parameterValues[parameter.id]);
+        }
+        // if we have a field id for this parameter, set "field_id"
+        const fieldId = getParameterTargetFieldId(parameter.target, card.dataset_query);
+        if (fieldId != null) {
+            parameter = assoc(parameter, "field_id", fieldId);
+        }
+        return parameter;
+    })
+}
+
 export function applyParameters(
     card: Card,
     parameters: Parameter[],
-    parameterValues: { [key: ParameterId]: string } = {},
+    parameterValues: ParameterValues = {},
     parameterMappings: ParameterMapping[] = []
 ): DatasetQuery {
     const datasetQuery = Utils.copy(card.dataset_query);
diff --git a/frontend/src/metabase/meta/Dashboard.js b/frontend/src/metabase/meta/Dashboard.js
index b03eacc7052c3aa1a49af63dd9b710b8e15ed86b..b2e86d69f20dc51ebd03b62cfcc0a24c244f4917 100644
--- a/frontend/src/metabase/meta/Dashboard.js
+++ b/frontend/src/metabase/meta/Dashboard.js
@@ -6,7 +6,7 @@ import type Field from "./metadata/Field";
 import type { FieldId } from "./types/Field";
 import type { TemplateTag } from "./types/Query";
 import type { Card } from "./types/Card";
-import type { ParameterOption, Parameter, ParameterType, ParameterMappingUIOption, ParameterMappingTarget, DimensionTarget, VariableTarget } from "./types/Dashboard";
+import type { ParameterOption, Parameter, ParameterType, ParameterMappingUIOption, DimensionTarget, VariableTarget } from "./types/Parameter";
 
 import { getTemplateTags } from "./Card";
 
@@ -206,23 +206,6 @@ export function getCardVariables(metadata: Metadata, card: Card, filter: Templat
     return [];
 }
 
-export function getParameterMappingTargetField(metadata: Metadata, card: Card, target: ParameterMappingTarget): ?Field {
-    if (target[0] === "dimension") {
-        let dimension = target[1];
-        if (Array.isArray(dimension) && mbqlEq(dimension[0], "template-tag")) {
-            if (card.dataset_query.type === "native") {
-                let templateTag = card.dataset_query.native.template_tags[String(dimension[1])];
-                if (templateTag && templateTag.type === "dimension") {
-                    return metadata.field(Query.getFieldTargetId(templateTag.dimension));
-                }
-            }
-        } else {
-            return metadata.field(Query.getFieldTargetId(dimension));
-        }
-    }
-    return null;
-}
-
 function fieldFilterForParameter(parameter: Parameter) {
     return fieldFilterForParameterType(parameter.type);
 }
diff --git a/frontend/src/metabase/meta/Parameter.js b/frontend/src/metabase/meta/Parameter.js
index 130ac93e4bdde4ee476af69733ccfc4303a35025..f0f8d946b05c043b5ca5a41dbe4aaaa96282f55e 100644
--- a/frontend/src/metabase/meta/Parameter.js
+++ b/frontend/src/metabase/meta/Parameter.js
@@ -1,11 +1,12 @@
 /* @flow */
 
+import type { DatasetQuery } from "./types/Card";
 import type { TemplateTag } from "./types/Query";
-import type { Parameter, ParameterId } from "./types/Dashboard";
+import type { Parameter, ParameterTarget, ParameterValues } from "./types/Parameter";
+import type { FieldId } from "./types/Field";
 
-export type ParameterValues = {
-    [id: ParameterId]: string
-};
+import Q from "metabase/lib/query";
+import { mbqlEq } from "metabase/lib/query/util";
 
 // NOTE: this should mirror `template-tag-parameters` in src/metabase/api/embed.clj
 export function getTemplateTagParameters(tags: TemplateTag[]): Parameter[] {
@@ -25,9 +26,27 @@ export function getTemplateTagParameters(tags: TemplateTag[]): Parameter[] {
 export const getParametersBySlug = (parameters: Parameter[], parameterValues: ParameterValues): {[key:string]: string} => {
     let result = {};
     for (const parameter of parameters) {
-        if (parameterValues[parameter.id] !== undefined) {
+        if (parameterValues[parameter.id] != undefined) {
             result[parameter.slug] = parameterValues[parameter.id];
         }
     }
     return result;
 }
+
+/** Returns the field ID that this parameter target points to, or null if it's not a dimension target. */
+export function getParameterTargetFieldId(target: ?ParameterTarget, datasetQuery: DatasetQuery): ?FieldId {
+    if (target && target[0] === "dimension") {
+        let dimension = target[1];
+        if (Array.isArray(dimension) && mbqlEq(dimension[0], "template-tag")) {
+            if (datasetQuery.type === "native") {
+                let templateTag = datasetQuery.native.template_tags[String(dimension[1])];
+                if (templateTag && templateTag.type === "dimension") {
+                    return Q.getFieldTargetId(templateTag.dimension);
+                }
+            }
+        } else {
+            return Q.getFieldTargetId(dimension);
+        }
+    }
+    return null;
+}
diff --git a/frontend/src/metabase/meta/metadata/Field.js b/frontend/src/metabase/meta/metadata/Field.js
index 5f6955d736ad1c50cad3d5b0b12a0b3d8dd71a63..5fdd57497135461159a543a735b13639269a524e 100644
--- a/frontend/src/metabase/meta/metadata/Field.js
+++ b/frontend/src/metabase/meta/metadata/Field.js
@@ -6,6 +6,7 @@ import Table from "./Table";
 import type { FieldId, Field as FieldObject } from "metabase/meta/types/Field";
 import type { TableId } from "metabase/meta/types/Table";
 
+import { getFieldValues } from "metabase/lib/query/field";
 import { isDate, isNumeric, isBoolean, isString, isSummable, isCategory, isDimension, isMetric, getIconForField } from "metabase/lib/schema_metadata";
 import { isPK, isFK } from "metabase/lib/types";
 
@@ -48,15 +49,7 @@ export default class Field extends Base {
     isFK()        { return isFK(this.special_type); }
 
     values(): Array<string> {
-        let values = this._object.values;
-        // https://github.com/metabase/metabase/issues/3417
-        if (Array.isArray(values)) {
-            return values;
-        } else if (values && Array.isArray(values.values)) {
-            return values.values;
-        } else {
-            return [];
-        }
+        return getFieldValues(this._object)
     }
 
     icon() {
diff --git a/frontend/src/metabase/meta/types/Card.js b/frontend/src/metabase/meta/types/Card.js
index 9e88a93182db1448efd36870f5da1be9b2e97e27..9ef102d6db3011f2e7e4ccfac89184ceca1e4819 100644
--- a/frontend/src/metabase/meta/types/Card.js
+++ b/frontend/src/metabase/meta/types/Card.js
@@ -2,7 +2,7 @@
 
 import type { DatabaseId } from "./Database";
 import type { StructuredQuery, NativeQuery } from "./Query";
-import type { Parameter, ParameterInstance } from "./Dashboard";
+import type { Parameter, ParameterInstance } from "./Parameter";
 
 export type CardId = number;
 
diff --git a/frontend/src/metabase/meta/types/Dashboard.js b/frontend/src/metabase/meta/types/Dashboard.js
index 5b44d74fd261dcdc82a987a3e9b24a32a511d6d2..a839d322b9bdda8bac9b4545e29ee910a068db41 100644
--- a/frontend/src/metabase/meta/types/Dashboard.js
+++ b/frontend/src/metabase/meta/types/Dashboard.js
@@ -1,18 +1,30 @@
 /* @flow */
 
 import type { Card, CardId, VisualizationSettings } from "./Card";
-
-import type { ConcreteField } from "./Query";
+import type { Parameter, ParameterMapping } from "./Parameter";
 
 export type DashboardId = number;
 
 export type Dashboard = {
     id: DashboardId,
     name: string,
+    created_at: ?string,
     description: ?string,
-    ordered_cards: Array<DashCard>,
+    caveats?: string,
+    points_of_interest?: string,
+    show_in_getting_started?: boolean,
     // incomplete
     parameters: Array<Parameter>
+}
+
+// TODO Atte Keinänen 4/5/16: After upgrading Flow, use spread operator `...Dashboard`
+export type DashboardWithCards = {
+    id: DashboardId,
+    name: string,
+    description: ?string,
+    ordered_cards: Array<DashCard>,
+    // incomplete
+    parameters: Array<Parameter>,
 };
 
 export type DashCardId = number;
@@ -27,7 +39,6 @@ export type DashCard = {
     series: Array<Card>,
 
     // incomplete
-
     parameter_mappings: Array<ParameterMapping>,
     visualization_settings: VisualizationSettings,
 
@@ -36,54 +47,3 @@ export type DashCard = {
     sizeY: number,
     sizeX: number
 };
-
-export type ParameterId = string;
-
-export type ParameterType = string;
-
-export type Parameter = {
-    id: ParameterId,
-    name: string,
-    type: ParameterType,
-    slug: string,
-    default?: string
-};
-
-export type VariableTarget = ["template-tag", string];
-export type DimensionTarget = ["template-tag", string] | ConcreteField
-
-export type ParameterMappingTarget =
-    ["variable", VariableTarget] |
-    ["dimension", DimensionTarget];
-
-export type ParameterMappingOption = {
-    name: string,
-    target: ParameterMappingTarget,
-};
-
-export type ParameterMapping = {
-    card_id: CardId,
-    parameter_id: ParameterId,
-    target: ParameterMappingTarget
-};
-
-export type ParameterOption = {
-    name: string,
-    description?: string,
-    type: ParameterType
-};
-
-export type ParameterInstance = {
-    type: ParameterType,
-    target: ParameterMappingTarget,
-    value: string
-};
-
-
-
-export type ParameterMappingUIOption = ParameterMappingOption & {
-    icon: ?string,
-    sectionName: string,
-    isFk?: boolean,
-    isVariable?: boolean,
-}
diff --git a/frontend/src/metabase/meta/types/Field.js b/frontend/src/metabase/meta/types/Field.js
index b0fe0b67557db926839568454712c9c0a3e24ed1..0166238a2f4fe4b42a56efb72a90d8a1bdf70887 100644
--- a/frontend/src/metabase/meta/types/Field.js
+++ b/frontend/src/metabase/meta/types/Field.js
@@ -6,6 +6,12 @@ export type FieldId = number;
 export type Field = {
     id: FieldId,
 
+    // Metadata field "values" type is inconsistent
     // https://github.com/metabase/metabase/issues/3417
-    values: Array<string> | { values: Array<string> }
+    values: [] | FieldValues
 };
+
+export type FieldValues = {
+    // incomplete
+    values: Array<string> | {}
+}
diff --git a/frontend/src/metabase/meta/types/Parameter.js b/frontend/src/metabase/meta/types/Parameter.js
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..207a938770a7596a2468ebbcfee71dfedc57bd2f 100644
--- a/frontend/src/metabase/meta/types/Parameter.js
+++ b/frontend/src/metabase/meta/types/Parameter.js
@@ -0,0 +1,60 @@
+/* @flow */
+
+import type { CardId } from "./Card";
+import type { ConcreteField } from "./Query";
+
+export type ParameterId = string;
+export type ParameterType = string;
+
+export type Parameter = {
+    id: ParameterId,
+    name: string,
+    type: ParameterType,
+    slug: string,
+    default?: string,
+
+    target?: ParameterTarget
+};
+
+export type ParameterValue = number | string;
+
+export type VariableTarget = ["template-tag", string];
+export type DimensionTarget = ["template-tag", string] | ConcreteField
+
+export type ParameterTarget =
+    ["variable", VariableTarget] |
+    ["dimension", DimensionTarget];
+
+export type ParameterMappingOption = {
+    name: string,
+    target: ParameterTarget,
+};
+
+export type ParameterMapping = {
+    card_id: CardId,
+    parameter_id: ParameterId,
+    target: ParameterTarget
+};
+
+export type ParameterOption = {
+    name: string,
+    description?: string,
+    type: ParameterType
+};
+
+export type ParameterInstance = {
+    type: ParameterType,
+    target: ParameterTarget,
+    value: string
+};
+
+export type ParameterMappingUIOption = ParameterMappingOption & {
+    icon: ?string,
+    sectionName: string,
+    isFk?: boolean,
+    isVariable?: boolean,
+};
+
+export type ParameterValues = {
+    [id: ParameterId]: ParameterValue
+};
diff --git a/frontend/src/metabase/meta/types/Query.js b/frontend/src/metabase/meta/types/Query.js
index 8f4ab74316c2417866e6f4efea6298e093391a60..d8223de5382cb90cf80204af9ef8a50c8eac4150 100644
--- a/frontend/src/metabase/meta/types/Query.js
+++ b/frontend/src/metabase/meta/types/Query.js
@@ -3,7 +3,7 @@
 import type { TableId } from "./Table";
 import type { FieldId } from "./Field";
 import type { SegmentId } from "./Segment";
-import type { ParameterType } from "./Dashboard";
+import type { ParameterType } from "./Parameter";
 
 export type MetricId = number;
 
@@ -21,13 +21,14 @@ export type RelativeDatetimeUnit = "minute" | "hour" | "day" | "week" | "month"
 export type DatetimeUnit = "default" | "minute" | "minute-of-hour" | "hour" | "hour-of-day" | "day" | "day-of-week" | "day-of-month" | "day-of-year" | "week" | "week-of-year" | "month" | "month-of-year" | "quarter" | "quarter-of-year" | "year";
 
 export type TemplateTagId = string;
+export type TemplateTagName = string;
 
 export type TemplateTag = {
     id:           TemplateTagId,
-    name:         string,
+    name:         TemplateTagName,
     display_name: string,
     type:         string,
-    dimension?:   ["field-id", number],
+    dimension?:   LocalFieldReference,
     widget_type?: ParameterType,
     required?:    boolean,
     default?:     string,
@@ -35,7 +36,7 @@ export type TemplateTag = {
 
 export type NativeQuery = {
     query: string,
-    template_tags: { [key: string]: TemplateTag }
+    template_tags: { [key: TemplateTagName]: TemplateTag }
 };
 
 export type StructuredQuery = {
diff --git a/frontend/src/metabase/meta/types/Revision.js b/frontend/src/metabase/meta/types/Revision.js
new file mode 100644
index 0000000000000000000000000000000000000000..9155fc83e33b376da71053414e913742caa0af75
--- /dev/null
+++ b/frontend/src/metabase/meta/types/Revision.js
@@ -0,0 +1,8 @@
+/* @flow */
+
+export type RevisionId = string;
+
+export type Revision = {
+    // TODO: incomplete
+    id: RevisionId
+}
diff --git a/frontend/src/metabase/meta/types/index.js b/frontend/src/metabase/meta/types/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..98c46550150e67ba9d08baf881217b911dc724fb
--- /dev/null
+++ b/frontend/src/metabase/meta/types/index.js
@@ -0,0 +1,26 @@
+/* @flow */
+
+// dashboard, card, etc
+export type EntityType = string;
+
+// DashboardId, CardId, etc
+export type EntityId = number;
+
+/* Location descriptor used by react-router and history */
+export type LocationDescriptor = {
+    hash: string,
+    pathname: string,
+    search?: string,
+    query?: { [key: string]: string }
+};
+
+/* Map of query string names to string values */
+export type QueryParams = {
+    [key: string]: string
+};
+
+/* Metabase API error object returned by the backend */
+export type ApiError = {
+    status: number, // HTTP status
+    // TODO: incomplete
+}
diff --git a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
deleted file mode 100644
index e465cf16b2855b99b620793e62aedc799ff5e05e..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
+++ /dev/null
@@ -1,167 +0,0 @@
-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 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";
-
-import { createDashboard, fetchDashboards } from "metabase/dashboard/dashboard";
-import { getDashboards } from "../selectors";
-
-const mapStateToProps = (state, props) => ({
-    dashboards: getDashboards(state)
-});
-
-const mapDispatchToProps = {
-    createDashboard,
-    fetchDashboards
-};
-
-@connect(mapStateToProps, mapDispatchToProps)
-export default class DashboardsDropdown extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            dropdownOpen: false,
-            modalOpen: false
-        };
-
-        this.styles = {
-            dashIcon: {
-                width: '105px',
-                height: '90px',
-                backgroundImage: 'url("app/assets/img/dash_empty_state.svg")',
-                backgroundRepeat: 'no-repeat'
-            }
-        }
-
-        _.bindAll(this, "toggleDropdown", "closeDropdown", "toggleModal", "closeModal");
-    }
-
-    static propTypes = {
-        dashboards: PropTypes.array.isRequired,
-        createDashboard: PropTypes.func.isRequired,
-        fetchDashboards: PropTypes.func.isRequired,
-    };
-
-    componentWillMount() {
-        this.props.fetchDashboards();
-    }
-
-    async onCreateDashboard(newDashboard) {
-        let { createDashboard } = this.props;
-
-        try {
-            let action = await createDashboard(newDashboard, true);
-            // FIXME: this doesn't feel right...
-            this.props.onChangeLocation(Urls.dashboard(action.payload.id));
-        } catch (e) {
-            console.log("createDashboard failed", e);
-        }
-
-        // close modal and add new dash to our dashboards list
-        this.setState({
-            dropdownOpen: false,
-            modalOpen: false
-        });
-
-        MetabaseAnalytics.trackEvent("Dashboard", "Create");
-    }
-
-    toggleModal() {
-        if (!this.state.modalOpen) {
-            // when we open our modal we always close the dropdown
-            this.setState({
-                dropdownOpen: false,
-                modalOpen: !this.state.modalOpen
-            });
-        }
-    }
-
-    closeModal() {
-        this.setState({ modalOpen: false });
-    }
-
-    toggleDropdown() {
-        this.setState({ dropdownOpen: !this.state.dropdownOpen });
-    }
-
-    closeDropdown() {
-        this.setState({ dropdownOpen: false });
-    }
-
-    renderCreateDashboardModal() {
-        return (
-            <Modal>
-                <CreateDashboardModal
-                    createDashboardFn={this.onCreateDashboard.bind(this)}
-                    onClose={this.closeModal} />
-            </Modal>
-        );
-    }
-
-    render() {
-        let { children, dashboards } = this.props;
-        let { dropdownOpen, modalOpen } = this.state;
-
-        return (
-            <div>
-                { modalOpen ? this.renderCreateDashboardModal() : null }
-                <div className={cx('NavDropdown inline-block cursor-pointer', { 'open': dropdownOpen })}>
-                    <span onClick={this.toggleDropdown}>
-                        {children}
-                    </span>
-
-                    { dropdownOpen ?
-                        <OnClickOutsideWrapper handleDismissal={this.closeDropdown}>
-                        <ConstrainToScreen>
-                            <div className="NavDropdown-content DashboardList NavDropdown-content--dashboards scroll-y">
-                                { dashboards.length === 0 ?
-                                    <div className="NavDropdown-content-layer text-white text-centered">
-                                        <div className="p2"><div style={this.styles.dashIcon} className="ml-auto mr-auto"></div></div>
-                                        <div className="px2 py1 text-bold">You don’t have any dashboards yet.</div>
-                                        <div className="px2 pb2">Dashboards group visualizations for frequent questions in a single handy place.</div>
-                                        <div className="border-top border-light">
-                                            <a className="Dropdown-item block text-white no-decoration" onClick={this.toggleModal}>Create your first dashboard</a>
-                                        </div>
-                                    </div>
-                                :
-                                    <ul className="NavDropdown-content-layer">
-                                        { dashboards.map(dash =>
-                                            <li key={dash.id} className="block">
-                                                <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>
-                                                    { dash.description ?
-                                                        <div className="mt1 text-brand-light">
-                                                            {dash.description}
-                                                        </div>
-                                                    : null }
-                                                </Link>
-                                            </li>
-                                        )}
-                                        <li className="block border-top border-light">
-                                            <a data-metabase-event={"Navbar;Dashboard Dropdown;Create Dashboard"} className="Dropdown-item block text-white no-decoration" onClick={this.toggleModal}>Create a new dashboard</a>
-                                        </li>
-                                    </ul>
-                                }
-                            </div>
-                        </ConstrainToScreen>
-                        </OnClickOutsideWrapper>
-                    : null }
-                </div>
-            </div>
-        );
-    }
-}
diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx
index ffc6dd9674b87acb30d3ee3139e569063fdbac84..843f3fe691325a8008c401163830a61acbefa0af 100644
--- a/frontend/src/metabase/nav/containers/Navbar.jsx
+++ b/frontend/src/metabase/nav/containers/Navbar.jsx
@@ -8,12 +8,10 @@ import { Link } from "react-router";
 
 import Icon from "metabase/components/Icon.jsx";
 import LogoIcon from "metabase/components/LogoIcon.jsx";
+import * as Urls from "metabase/lib/urls";
 
-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) => ({
@@ -26,17 +24,44 @@ const mapDispatchToProps = {
     onChangeLocation: push
 };
 
+const BUTTON_PADDING_STYLES = {
+    navButton: {
+        paddingLeft: "1.0rem",
+        paddingRight: "1.0rem",
+        paddingTop: "0.75rem",
+        paddingBottom: "0.75rem"
+    },
+
+    newQuestion: {
+        paddingLeft: "1.0rem",
+        paddingRight: "1.0rem",
+        paddingTop: "0.75rem",
+        paddingBottom: "0.75rem",
+    }
+};
+
 const AdminNavItem = ({ name, path, currentPath }) =>
     <li>
         <Link
             to={path}
-            data-metabase-event={"Navbar;" + name}
+            data-metabase-event={`NavBar;${name}`}
             className={cx("NavItem py1 px2 no-decoration", {"is--selected": currentPath.startsWith(path) })}
         >
             {name}
         </Link>
     </li>
 
+const MainNavLink = ({ to, name, eventName }) =>
+    <Link
+        to={to}
+        data-metabase-event={`NavBar;${eventName}`}
+        style={BUTTON_PADDING_STYLES.navButton}
+        className={"NavItem cursor-pointer text-white text-bold no-decoration flex align-center px2 transition-background"}
+        activeClassName="NavItem--selected"
+    >
+        {name}
+    </Link>
+
 @connect(mapStateToProps, mapDispatchToProps)
 export default class Navbar extends Component {
     static propTypes = {
@@ -48,22 +73,6 @@ export default class Navbar extends Component {
 
     constructor(props, context) {
         super(props, context);
-
-        this.styles = {
-            navButton: {
-                paddingLeft: "1.0rem",
-                paddingRight: "1.0rem",
-                paddingTop: "0.75rem",
-                paddingBottom: "0.75rem"
-            },
-
-            newQuestion: {
-                paddingLeft: "1.0rem",
-                paddingRight: "1.0rem",
-                paddingTop: "0.75rem",
-                paddingBottom: "0.75rem",
-            }
-        };
     }
 
     isActive(path) {
@@ -109,7 +118,7 @@ export default class Navbar extends Component {
 
     renderMainNav() {
         return (
-            <nav className={cx("Nav CheckBg CheckBg-offset relative bg-brand sm-py2 sm-py1 xl-py3", this.props.className)}>
+            <nav className={cx("Nav relative bg-brand sm-py2 sm-py1 xl-py3", this.props.className)}>
                 <ul className="ml2 sm-pl4 pr1 flex align-center">
                     <li>
                         <Link to="/" data-metabase-event={"Navbar;Logo"} className="NavItem cursor-pointer text-white flex align-center my1 transition-background p1">
@@ -117,32 +126,19 @@ export default class Navbar extends Component {
                         </Link>
                     </li>
                     <li className="pl3 hide sm-show">
-                        <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("/dashboard/")
-                                })}
-                            >
-                                <span className="NavDropdown-button-layer">
-                                    Dashboards
-                                    <Icon className="ml1" name={'chevrondown'} size={8}></Icon>
-                                </span>
-                            </a>
-                        </DashboardsDropdown>
+                        <MainNavLink to="/dashboard" name="Dashboards" eventName="Dashboards" />
                     </li>
                     <li className="pl1 hide sm-show">
-                        <Link to="/questions" data-metabase-event={"Navbar;Questions"} style={this.styles.navButton} className={cx("NavItem cursor-pointer text-white text-bold no-decoration flex align-center px2 transition-background")} activeClassName="NavItem--selected">Questions</Link>
+                        <MainNavLink to="/questions" name="Questions" eventName="Questions" />
                     </li>
                     <li className="pl1 hide sm-show">
-                        <Link to="/pulse" data-metabase-event={"Navbar;Pulses"} style={this.styles.navButton} className={cx("NavItem cursor-pointer text-white text-bold no-decoration flex align-center px2 transition-background")} activeClassName="NavItem--selected">Pulses</Link>
+                        <MainNavLink to="/pulse" name="Pulses" eventName="Pulses" />
                     </li>
                     <li className="pl1 hide sm-show">
-                        <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>
+                        <MainNavLink to="/reference/guide" name="Data Reference" eventName="DataReference" />
                     </li>
                     <li className="pl3 hide sm-show">
-                        <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">
+                        <Link to={Urls.question()} data-metabase-event={"Navbar;New Question"} style={BUTTON_PADDING_STYLES.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">
                             New <span>Question</span>
                         </Link>
                     </li>
diff --git a/frontend/src/metabase/nav/selectors.js b/frontend/src/metabase/nav/selectors.js
index 72255cc72213ffb23e74011d5541c1fb9aad2c76..4b958f0fca83163976e82a5d43be55ff391dbff0 100644
--- a/frontend/src/metabase/nav/selectors.js
+++ b/frontend/src/metabase/nav/selectors.js
@@ -19,5 +19,3 @@ export const getContext = createSelector(
         :
             'main'
 );
-
-export const getDashboards = (state) => state.dashboard.dashboardListing;
diff --git a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx b/frontend/src/metabase/parameters/components/ParameterValueWidget.jsx
similarity index 86%
rename from frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
rename to frontend/src/metabase/parameters/components/ParameterValueWidget.jsx
index c7d44bf601f85dfca12787a1fff1c65bc5977dec..6c9a4ba76db65b32cdccb8368310042c872ddec1 100644
--- a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
+++ b/frontend/src/metabase/parameters/components/ParameterValueWidget.jsx
@@ -1,6 +1,8 @@
 /* eslint "react/prop-types": "warn" */
-import React, {Component} from "react"
+
+import React, { Component } from "react"
 import PropTypes from "prop-types";
+import { connect } from "react-redux";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import Icon from "metabase/components/Icon.jsx";
@@ -14,7 +16,7 @@ import DateAllOptionsWidget from "./widgets/DateAllOptionsWidget.jsx";
 import CategoryWidget from "./widgets/CategoryWidget.jsx";
 import TextWidget from "./widgets/TextWidget.jsx";
 
-import S from "../../containers/ParameterWidget.css";
+import S from "./ParameterWidget.css";
 
 import cx from "classnames";
 
@@ -27,6 +29,18 @@ const WIDGETS = {
     "date/all-options": DateAllOptionsWidget
 }
 
+import { fetchFieldValues } from "metabase/redux/metadata";
+import { getParameterFieldValues } from "metabase/selectors/metadata";
+
+const mapStateToProps = (state, props) => ({
+    values: getParameterFieldValues(state, props),
+})
+
+const mapDispatchToProps = {
+    fetchFieldValues
+}
+
+@connect(mapStateToProps, mapDispatchToProps)
 export default class ParameterValueWidget extends Component {
 
     static propTypes = {
@@ -71,6 +85,22 @@ export default class ParameterValueWidget extends Component {
 
     state = { isFocused: false };
 
+    componentWillMount() {
+        this.updateFieldValues(this.props);
+    }
+
+    componentWillReceiveProps(nextProps) {
+        if (nextProps.parameter.field_id != null && nextProps.parameter.field_id !== this.props.parameter.field_id) {
+            this.updateFieldValues(nextProps);
+        }
+    }
+
+    updateFieldValues(props) {
+        if (props.parameter.field_id != null) {
+            props.fetchFieldValues(props.parameter.field_id)
+        }
+    }
+
     render() {
         const {parameter, value, values, setValue, isEditing, placeholder, isFullscreen,
                noReset, commitImmediately, className, focusChanged: parentFocusChanged} = this.props;
diff --git a/frontend/src/metabase/dashboard/containers/ParameterWidget.css b/frontend/src/metabase/parameters/components/ParameterWidget.css
similarity index 100%
rename from frontend/src/metabase/dashboard/containers/ParameterWidget.css
rename to frontend/src/metabase/parameters/components/ParameterWidget.css
diff --git a/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx b/frontend/src/metabase/parameters/components/ParameterWidget.jsx
similarity index 81%
rename from frontend/src/metabase/dashboard/containers/ParameterWidget.jsx
rename to frontend/src/metabase/parameters/components/ParameterWidget.jsx
index 9f7ab1ba4a955fc5f7edd74b0fcb3219083b4338..05d13def786334bbfe1649d65d4b28f6fceb7c52 100644
--- a/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx
+++ b/frontend/src/metabase/parameters/components/ParameterWidget.jsx
@@ -1,8 +1,8 @@
-import React, {Component} from 'react';
+
+import React, { Component } from 'react';
 import PropTypes from "prop-types";
-import {connect} from "react-redux";
 
-import ParameterValueWidget from "../components/parameters/ParameterValueWidget.jsx";
+import ParameterValueWidget from "./ParameterValueWidget.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
 import S from "./ParameterWidget.css";
@@ -11,29 +11,11 @@ import _ from "underscore";
 
 import FieldSet from "../../components/FieldSet";
 
-import {getMappingsByParameter} from "../selectors";
-
-const makeMapStateToProps = () => {
-    const mapStateToProps = (state, props) => ({
-        mappingsByParameter: getMappingsByParameter(state, props)
-    });
-    return mapStateToProps;
-}
-
-const mapDispatchToProps = {};
-
-
-@connect(makeMapStateToProps, mapDispatchToProps)
 export default class ParameterWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            isEditingName: false,
-            isFocused: false
-        };
-
-        this.focusChanged = this.focusChanged.bind(this)
-    }
+    state = {
+        isEditingName: false,
+        isFocused: false
+    };
 
     static propTypes = {
         parameter: PropTypes.object,
@@ -45,28 +27,14 @@ export default class ParameterWidget extends Component {
         commitImmediately: false
     }
 
-    getValues() {
-        const {parameter, mappingsByParameter} = this.props;
-        return _.chain(mappingsByParameter[parameter.id])
-            .map(_.values)
-            .flatten()
-            .map(m => m.values || [])
-            .flatten()
-            .sortBy(_.identity)
-            .uniq(true)
-            .value();
-    }
-
     renderPopover(value, setValue, placeholder, isFullscreen) {
         const {parameter, editingParameter, commitImmediately} = this.props;
         const isEditingParameter = !!(editingParameter && editingParameter.id === parameter.id);
-        const values = this.getValues();
         return (
             <ParameterValueWidget
                 parameter={parameter}
                 name={name}
                 value={value}
-                values={values}
                 setValue={setValue}
                 isEditing={isEditingParameter}
                 placeholder={placeholder}
@@ -77,7 +45,7 @@ export default class ParameterWidget extends Component {
         );
     }
 
-    focusChanged(isFocused) {
+    focusChanged = (isFocused) => {
         this.setState({isFocused})
     }
 
diff --git a/frontend/src/metabase/dashboard/containers/Parameters.jsx b/frontend/src/metabase/parameters/components/Parameters.jsx
similarity index 74%
rename from frontend/src/metabase/dashboard/containers/Parameters.jsx
rename to frontend/src/metabase/parameters/components/Parameters.jsx
index 80a6b3963078581605584f5b654bb11ab84a74de..22ab38f1d391edaea7f7e00527308b47fec1c8d6 100644
--- a/frontend/src/metabase/dashboard/containers/Parameters.jsx
+++ b/frontend/src/metabase/parameters/components/Parameters.jsx
@@ -1,4 +1,4 @@
-/* @flow weak */
+/* @flow */
 
 import React, { Component } from "react";
 
@@ -7,7 +7,33 @@ import ParameterWidget from "./ParameterWidget.jsx";
 import querystring from "querystring";
 import cx from "classnames";
 
-export default class Parameters extends Component {
+import type { QueryParams } from "metabase/meta/types";
+import type { ParameterId, Parameter, ParameterValues } from "metabase/meta/types/Parameter";
+
+type Props = {
+    className?:                 string,
+
+    parameters:                 Parameter[],
+    editingParameter:           ?Parameter,
+    parameterValues:            ParameterValues,
+
+    isFullscreen?:              boolean,
+    isNightMode?:               boolean,
+    isEditing?:                 boolean,
+    isQB?:                      boolean,
+    vertical?:                  boolean,
+    commitImmediately?:         boolean,
+
+    query:                      QueryParams,
+
+    setParameterName:           (parameterId: ParameterId, name: string) => void,
+    setParameterValue:          (parameterId: ParameterId, value: string) => void,
+    setParameterDefaultValue:   (parameterId: ParameterId, defaultValue: string) => void,
+    removeParameter:            (parameterId: ParameterId) => void,
+    setEditingParameter:        (parameterId: ParameterId) => void,
+}
+
+export default class Parameters extends Component<*, Props, *> {
     defaultProps = {
         syncQueryString: false,
         vertical: false,
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx b/frontend/src/metabase/parameters/components/widgets/CategoryWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/CategoryWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateAllOptionsWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/DateAllOptionsWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateMonthYearWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/DateMonthYearWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateQuarterYearWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/DateQuarterYearWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateRangeWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/DateRangeWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateRelativeWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/DateRelativeWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateSingleWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/DateSingleWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx b/frontend/src/metabase/parameters/components/widgets/TextWidget.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx
rename to frontend/src/metabase/parameters/components/widgets/TextWidget.jsx
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx b/frontend/src/metabase/parameters/components/widgets/YearPicker.jsx
similarity index 100%
rename from frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx
rename to frontend/src/metabase/parameters/components/widgets/YearPicker.jsx
diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx
index b53eee3d021b335c16a31e985ecc16ae37ffbe51..76c9443c931cdd4e0c6bf1d1b88a593e4e201cae 100644
--- a/frontend/src/metabase/public/components/EmbedFrame.jsx
+++ b/frontend/src/metabase/public/components/EmbedFrame.jsx
@@ -1,12 +1,14 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { withRouter } from "react-router"; import { IFRAMED } from "metabase/lib/dom";
+import { withRouter } from "react-router";
 
-import Parameters from "metabase/dashboard/containers/Parameters";
+import { IFRAMED } from "metabase/lib/dom";
+import { parseHashOptions } from "metabase/lib/browser";
+
+import Parameters from "metabase/parameters/components/Parameters";
 import LogoBadge from "./LogoBadge";
 
-import querystring from "querystring";
 import cx from "classnames";
 
 import "./EmbedFrame.css";
@@ -16,7 +18,7 @@ const DEFAULT_OPTIONS = {
     titled: true
 }
 
-import type { Parameter } from "metabase/meta/types/Dashboard";
+import type { Parameter } from "metabase/meta/types/Parameter";
 
 type Props = {
     className?: string,
@@ -24,7 +26,7 @@ type Props = {
     actionButtons?: any[],
     name?: string,
     description?: string,
-    location: { query: {[key:string]: string}},
+    location: { query: {[key:string]: string}, hash: string },
     parameters?: Parameter[],
     parameterValues?: {[key:string]: string},
     setParameterValue: (id: string, value: string) => void
@@ -54,23 +56,13 @@ export default class EmbedFrame extends Component<*, Props, *> {
         }
     }
 
-    _getOptions() {
-        let options = querystring.parse(window.location.hash.replace(/^#/, ""));
-        for (var name in options) {
-            if (/^(true|false|-?\d+(\.\d+)?)$/.test(options[name])) {
-                options[name] = JSON.parse(options[name]);
-            }
-        }
-        return { ...DEFAULT_OPTIONS, ...options };
-    }
-
     render() {
         const { className, children, actionButtons, location, parameters, parameterValues, setParameterValue } = this.props;
         const { innerScroll } = this.state;
 
         const footer = true;
 
-        const { bordered, titled, theme } = this._getOptions();
+        const { bordered, titled, theme } = { ...DEFAULT_OPTIONS, ...parseHashOptions(location.hash) };
 
         const name = titled ? this.props.name : null;
 
diff --git a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
index f5f4d111e1c43a7e643ebcb2035e79ebb0b7e91b..cf40db04da529ba048b7f46e571f73421825c64a 100644
--- a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
@@ -10,7 +10,7 @@ import AdvancedSettingsPane from "./AdvancedSettingsPane";
 import PreviewPane from "./PreviewPane";
 import EmbedCodePane from "./EmbedCodePane";
 
-import type { Parameter, ParameterId } from "metabase/meta/types/Dashboard";
+import type { Parameter, ParameterId } from "metabase/meta/types/Parameter";
 import type { Pane, EmbedType, EmbeddableResource, EmbeddingParams, DisplayOptions } from "./EmbedModalContent";
 
 import _ from "underscore";
diff --git a/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx b/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx
index e8abc5179ba56393212673124c34d9670fd2366f..79af697674fa352f5b211795bab1ef8206a35a42 100644
--- a/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx
@@ -4,7 +4,7 @@ import React from "react";
 
 import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
-import Parameters from "metabase/dashboard/containers/Parameters";
+import Parameters from "metabase/parameters/components/Parameters";
 import Select, { Option } from "metabase/components/Select";
 
 import DisplayOptionsPane from "./DisplayOptionsPane";
@@ -17,7 +17,7 @@ const getIconForParameter = (parameter) =>
     "unknown";
 
 import type { EmbedType, EmbeddableResource, EmbeddingParams, DisplayOptions } from "./EmbedModalContent";
-import type { Parameter, ParameterId } from "metabase/meta/types/Dashboard";
+import type { Parameter, ParameterId } from "metabase/meta/types/Parameter";
 
 type Props = {
     className?: string,
diff --git a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
index e46b9fb677fc6188dca638e23fd76bf77498dd4d..8e45a64699fdb94deee675e4f1903c6d780ca7a9 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
@@ -14,7 +14,7 @@ import { getSignedPreviewUrl, getUnsignedPreviewUrl, getSignedToken } from "meta
 import { getSiteUrl, getEmbeddingSecretKey, getIsPublicSharingEnabled, getIsApplicationEmbeddingEnabled } from "metabase/selectors/settings";
 import { getUserIsAdmin } from "metabase/selectors/user";
 
-import type { Parameter, ParameterId } from "metabase/meta/types/Dashboard";
+import type { Parameter, ParameterId } from "metabase/meta/types/Parameter";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
@@ -163,42 +163,50 @@ export default class EmbedModalContent extends Component<*, Props, State> {
                         }}
                     />
                 </div>
-                <div className="flex flex-full">
-                    { embedType == null ?
-                        <div className="flex-full ml-auto mr-auto" style={{ maxWidth: 1040 }}>
+                { embedType == null ?
+                    <div className="flex-full">
+                        {/* Center only using margins because  */}
+                        <div className="ml-auto mr-auto" style={{maxWidth: 1040}}>
                             <SharingPane
                                 {...this.props}
                                 publicUrl={getUnsignedPreviewUrl(siteUrl, resourceType, resource.public_uuid, displayOptions)}
                                 iframeUrl={getUnsignedPreviewUrl(siteUrl, resourceType, resource.public_uuid, displayOptions)}
-                                onChangeEmbedType={(embedType) => this.setState({ embedType })}
+                                onChangeEmbedType={(embedType) => this.setState({embedType})}
                             />
                         </div>
+                    </div>
                     : embedType === "application" ?
-                        <AdvancedEmbedPane
-                            pane={pane}
-                            resource={resource}
-                            resourceType={resourceType}
-                            embedType={embedType}
-                            token={getSignedToken(resourceType, resource.id, params, secretKey, embeddingParams)}
-                            iframeUrl={getSignedPreviewUrl(siteUrl, resourceType, resource.id, params, displayOptions, secretKey, embeddingParams)}
-                            siteUrl={siteUrl}
-                            secretKey={secretKey}
-                            params={params}
-                            displayOptions={displayOptions}
-                            previewParameters={previewParameters}
-                            parameterValues={parameterValues}
-                            resourceParameters={resourceParameters}
-                            embeddingParams={embeddingParams}
-                            onChangeDisplayOptions={(displayOptions) => this.setState({ displayOptions })}
-                            onChangeEmbeddingParameters={(embeddingParams) => this.setState({ embeddingParams })}
-                            onChangeParameterValue={(id, value) => this.setState({ parameterValues: { ...parameterValues, [id]: value }})}
-                            onChangePane={(pane) => this.setState({ pane })}
-                            onSave={this.handleSave}
-                            onUnpublish={this.handleUnpublish}
-                            onDiscard={this.handleDiscard}
-                        />
-                : null }
-                </div>
+                        <div className="flex flex-full">
+                            <AdvancedEmbedPane
+                                pane={pane}
+                                resource={resource}
+                                resourceType={resourceType}
+                                embedType={embedType}
+                                token={getSignedToken(resourceType, resource.id, params, secretKey, embeddingParams)}
+                                iframeUrl={getSignedPreviewUrl(siteUrl, resourceType, resource.id, params, displayOptions, secretKey, embeddingParams)}
+                                siteUrl={siteUrl}
+                                secretKey={secretKey}
+                                params={params}
+                                displayOptions={displayOptions}
+                                previewParameters={previewParameters}
+                                parameterValues={parameterValues}
+                                resourceParameters={resourceParameters}
+                                embeddingParams={embeddingParams}
+                                onChangeDisplayOptions={(displayOptions) => this.setState({displayOptions})}
+                                onChangeEmbeddingParameters={(embeddingParams) => this.setState({embeddingParams})}
+                                onChangeParameterValue={(id, value) => this.setState({
+                                    parameterValues: {
+                                        ...parameterValues,
+                                        [id]: value
+                                    }
+                                })}
+                                onChangePane={(pane) => this.setState({pane})}
+                                onSave={this.handleSave}
+                                onUnpublish={this.handleUnpublish}
+                                onDiscard={this.handleDiscard}
+                            />
+                        </div>
+                        : null }
             </div>
         );
     }
diff --git a/frontend/src/metabase/public/containers/PublicDashboard.jsx b/frontend/src/metabase/public/containers/PublicDashboard.jsx
index 8e4e688aa1fd990217ac551530c536d83e945fba..c74d4efe4133b027c2393beb32be77d946dee10a 100644
--- a/frontend/src/metabase/public/containers/PublicDashboard.jsx
+++ b/frontend/src/metabase/public/containers/PublicDashboard.jsx
@@ -4,27 +4,33 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import DashboardGrid from "metabase/dashboard/components/DashboardGrid.jsx";
+import { IFRAMED } from "metabase/lib/dom";
 
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import DashboardGrid from "metabase/dashboard/components/DashboardGrid";
+import DashboardControls from "metabase/dashboard/hoc/DashboardControls";
+import { getDashboardActions } from "metabase/dashboard/components/DashboardActions";
 import EmbedFrame from "../components/EmbedFrame";
 
 import { fetchDatabaseMetadata } from "metabase/redux/metadata";
 import { setErrorPage } from "metabase/redux/app";
 
-import { getDashboardComplete, getCardData, getCardDurations,getParameterValues } from "metabase/dashboard/selectors";
+import { getDashboardComplete, getCardData, getCardDurations, getParameters, getParameterValues } from "metabase/dashboard/selectors";
 
 import * as dashboardActions from "metabase/dashboard/dashboard";
 
 import type { Dashboard } from "metabase/meta/types/Dashboard";
+import type { Parameter } from "metabase/meta/types/Parameter";
 
 import _ from "underscore";
 
 const mapStateToProps = (state, props) => {
   return {
+      dashboardId:          props.params.dashboardId || props.params.uuid || props.params.token,
       dashboard:            getDashboardComplete(state, props),
       dashcardData:         getCardData(state, props),
       cardDurations:        getCardDurations(state, props),
+      parameters:           getParameters(state, props),
       parameterValues:      getParameterValues(state, props)
   }
 }
@@ -39,8 +45,10 @@ const mapDispatchToProps = {
 type Props = {
     params:                 { uuid?: string, token?: string },
     location:               { query: { [key:string]: string }},
+    dashboardId:            string,
 
     dashboard?:             Dashboard,
+    parameters:             Parameter[],
     parameterValues:        {[key:string]: string},
 
     initialize:             () => void,
@@ -51,6 +59,7 @@ type Props = {
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
+@DashboardControls
 export default class PublicDashboard extends Component<*, Props, *> {
     // $FlowFixMe
     async componentWillMount() {
@@ -72,14 +81,23 @@ export default class PublicDashboard extends Component<*, Props, *> {
     }
 
     render() {
-        const { dashboard, parameterValues } = this.props;
+        const { dashboard, parameters, parameterValues } = this.props;
+        const buttons = !IFRAMED ? getDashboardActions(this.props) : [];
+
         return (
             <EmbedFrame
                 name={dashboard && dashboard.name}
                 description={dashboard && dashboard.description}
-                parameters={dashboard && dashboard.parameters}
+                parameters={parameters}
                 parameterValues={parameterValues}
                 setParameterValue={this.props.setParameterValue}
+                actionButtons={buttons.length > 0 &&
+                    <div>
+                        {buttons.map((button, index) =>
+                            <span key={index} className="m1">{button}</span>
+                        )}
+                    </div>
+                }
             >
                 <LoadingAndErrorWrapper className="p1 flex-full" loading={!dashboard}>
                 { () =>
diff --git a/frontend/src/metabase/public/containers/PublicQuestion.jsx b/frontend/src/metabase/public/containers/PublicQuestion.jsx
index cdbba6e144db90b25de5ea9b6502939acc18f19b..21123ee7ab4225c82bb96c680ccdaf3e6564cd57 100644
--- a/frontend/src/metabase/public/containers/PublicQuestion.jsx
+++ b/frontend/src/metabase/public/containers/PublicQuestion.jsx
@@ -9,35 +9,38 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 import ExplicitSize from "metabase/components/ExplicitSize";
 import EmbedFrame from "../components/EmbedFrame";
 
-import { getParametersBySlug } from "metabase/meta/Parameter";
-
 import type { Card } from "metabase/meta/types/Card";
 import type { Dataset } from "metabase/meta/types/Dataset";
+import type { ParameterValues } from "metabase/meta/types/Parameter";
 
-import { getParameters, applyParameters } from "metabase/meta/Card";
+import { getParametersBySlug } from "metabase/meta/Parameter";
+import { getParameters, getParametersWithExtras, applyParameters } from "metabase/meta/Card";
 
 import { PublicApi, EmbedApi } from "metabase/services";
 
 import { setErrorPage } from "metabase/redux/app";
+import { addParamValues } from "metabase/redux/metadata";
 
 import { updateIn } from "icepick";
 
 type Props = {
-    params:       { uuid?: string, token?: string },
-    location:     { query: { [key:string]: string }},
-    width:        number,
-    height:       number,
-    setErrorPage: (error: { status: number }) => void,
+    params:         { uuid?: string, token?: string },
+    location:       { query: { [key:string]: string }},
+    width:          number,
+    height:         number,
+    setErrorPage:   (error: { status: number }) => void,
+    addParamValues: (any) => void
 };
 
 type State = {
     card:               ?Card,
     result:             ?Dataset,
-    parameterValues:    {[key:string]: string}
+    parameterValues:    ParameterValues
 };
 
 const mapDispatchToProps = {
-    setErrorPage
+    setErrorPage,
+    addParamValues
 };
 
 @connect(null, mapDispatchToProps)
@@ -68,9 +71,13 @@ export default class PublicQuestion extends Component<*, Props, State> {
                 throw { status: 404 }
             }
 
-            let parameterValues = {};
+            if (card.param_values) {
+                this.props.addParamValues(card.param_values);
+            }
+
+            let parameterValues: ParameterValues = {};
             for (let parameter of getParameters(card)) {
-                parameterValues[parameter.id] = query[parameter.slug];
+                parameterValues[String(parameter.id)] = query[parameter.slug];
             }
 
             this.setState({ card, parameterValues }, this.run);
@@ -143,7 +150,7 @@ export default class PublicQuestion extends Component<*, Props, State> {
             <EmbedFrame
                 name={card && card.name}
                 description={card && card.description}
-                parameters={card && card.parameters}
+                parameters={card && getParametersWithExtras(card)}
                 actionButtons={actionButtons}
                 parameterValues={parameterValues}
                 setParameterValue={this.setParameterValue}
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index ecfa64d17dd72b8c416c139e6e67dc9b05799b9f..0934529e2ef5429df334868cdbaa8c3bb0505589 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -34,7 +34,7 @@ import { assocIn } from "icepick";
 
 import DataSelector from './DataSelector.jsx';
 import Icon from "metabase/components/Icon.jsx";
-import Parameters from "metabase/dashboard/containers/Parameters";
+import Parameters from "metabase/parameters/components/Parameters";
 
 // This should return an object with information about the mode the ACE Editor should use to edit the query.
 // This object should have 2 properties:
diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
index 046d39df20344c0c18fe931d4b1d872140ad83ca..b14a37345919a64d07609bdcc88db9e159977700 100644
--- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
@@ -5,7 +5,7 @@ import { Link } from "react-router";
 import QueryModeButton from "./QueryModeButton.jsx";
 
 import ActionButton from 'metabase/components/ActionButton.jsx';
-import AddToDashSelectDashModal from 'metabase/components/AddToDashSelectDashModal.jsx';
+import AddToDashSelectDashModal from 'metabase/containers/AddToDashSelectDashModal.jsx';
 import ButtonBar from "metabase/components/ButtonBar.jsx";
 import DeleteQuestionModal from 'metabase/components/DeleteQuestionModal.jsx';
 import HeaderBar from "metabase/components/HeaderBar.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 75cc35c13acf85a134fcbb12305779dc4782f7c4..da14d60013de2d5b94be226abf5fef1152f1d43e 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 
 import Toggle from "metabase/components/Toggle.jsx";
 import Select, { Option } from "metabase/components/Select.jsx";
-import ParameterValueWidget from "metabase/dashboard/components/parameters/ParameterValueWidget.jsx";
+import ParameterValueWidget from "metabase/parameters/components/ParameterValueWidget.jsx";
 
 import { parameterOptionsForField } from "metabase/meta/Dashboard";
 import Field from "metabase/meta/metadata/Field";
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 0a51cc4f03de6fac8e7ed6d5a139fbeea7883ca4..7307bcd70109efa763b41d01ebe13686f17c1322 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -36,7 +36,7 @@ import {
     getTableForeignKeys,
     getTableForeignKeyReferences,
     getUiControls,
-    getParametersWithValues,
+    getParameters,
     getDatabaseFields,
     getSampleDatasetId,
     getNativeDatabases,
@@ -99,7 +99,7 @@ const mapStateToProps = (state, props) => {
         isObjectDetail:            getIsObjectDetail(state),
 
         uiControls:                getUiControls(state),
-        parameters:                getParametersWithValues(state),
+        parameters:                getParameters(state),
         databaseFields:            getDatabaseFields(state),
         sampleDatasetId:           getSampleDatasetId(state),
 
diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js
index 9f3472fe8c25ac37b183a8b487b70fdb132c6afb..09bfb004731b2709e4782e65b23138f46671073c 100644
--- a/frontend/src/metabase/query_builder/selectors.js
+++ b/frontend/src/metabase/query_builder/selectors.js
@@ -2,8 +2,7 @@
 import { createSelector } from "reselect";
 import _ from "underscore";
 
-import { getTemplateTags } from "metabase/meta/Card";
-import { getTemplateTagParameters } from "metabase/meta/Parameter";
+import { getParametersWithExtras } from "metabase/meta/Card";
 
 import { isCardDirty, isCardRunnable } from "metabase/lib/card";
 import { parseFieldTargetId } from "metabase/lib/query_time";
@@ -139,32 +138,9 @@ export const getMode = createSelector(
     (card, tableMetadata) => getMode_(card, tableMetadata)
 )
 
-export const getImplicitParameters = createSelector(
-    [getCard],
-    (card) =>
-        getTemplateTagParameters(getTemplateTags(card))
-);
-
-export const getModeParameters = createSelector(
-    [getLastRunCard, getTableMetadata, getMode],
-    (card, tableMetadata, mode) =>
-        (card && tableMetadata && mode && mode.getModeParameters) ?
-            mode.getModeParameters(card, tableMetadata) :
-            []
-);
-
 export const getParameters = createSelector(
-    [getModeParameters, getImplicitParameters],
-    (modeParameters, implicitParameters) => [...modeParameters, ...implicitParameters]
-);
-
-export const getParametersWithValues = createSelector(
-    [getParameters, getParameterValues],
-    (parameters, values) =>
-        parameters.map(parameter => ({
-            ...parameter,
-            value: values[parameter.id] != null ? values[parameter.id] : null
-        }))
+    [getCard, getParameterValues],
+    (card, parameterValues) => getParametersWithExtras(card, parameterValues)
 );
 
 export const getIsRunnable = createSelector(
@@ -177,7 +153,7 @@ const getNextRunDatasetQuery = createSelector([getCard], (card) => card && card.
 
 const getLastRunParameters = createSelector([getQueryResult], (queryResult) => queryResult && queryResult.json_query.parameters || [])
 const getLastRunParameterValues = createSelector([getLastRunParameters], (parameters) => parameters.map(parameter => parameter.value))
-const getNextRunParameterValues = createSelector([getParametersWithValues], (parameters) => parameters.map(parameter => parameter.value))
+const getNextRunParameterValues = createSelector([getParameters], (parameters) => parameters.map(parameter => parameter.value))
 
 export const getIsResultDirty = createSelector(
     [getLastRunDatasetQuery, getNextRunDatasetQuery, getLastRunParameterValues, getNextRunParameterValues],
diff --git a/frontend/src/metabase/questions/components/Item.jsx b/frontend/src/metabase/questions/components/Item.jsx
index 533016e71cbe52afa983ed79b5bdc7734c6bea67..d9979096454388417cd7bd8f7c84c2ce5764cc6d 100644
--- a/frontend/src/metabase/questions/components/Item.jsx
+++ b/frontend/src/metabase/questions/components/Item.jsx
@@ -113,9 +113,9 @@ Item.propTypes = {
     favorite:           PropTypes.bool.isRequired,
     archived:           PropTypes.bool.isRequired,
     icon:               PropTypes.string.isRequired,
-    setItemSelected:    PropTypes.func.isRequired,
-    setFavorited:       PropTypes.func.isRequired,
-    setArchived:        PropTypes.func.isRequired,
+    setItemSelected:    PropTypes.func,
+    setFavorited:       PropTypes.func,
+    setArchived:        PropTypes.func,
     onEntityClick:      PropTypes.func,
     showCollectionName: PropTypes.bool,
 };
@@ -156,7 +156,7 @@ ItemBody.propTypes = {
     favorite:           PropTypes.bool.isRequired,
     id:                 PropTypes.number.isRequired,
     name:               PropTypes.string.isRequired,
-    setFavorited:       PropTypes.func.isRequired,
+    setFavorited:       PropTypes.func,
 };
 
 const ItemCreated = pure(({ created, by }) =>
diff --git a/frontend/src/metabase/questions/containers/Archive.jsx b/frontend/src/metabase/questions/containers/Archive.jsx
index 595b0d1a2b777cae51ed8495e5e4c8d03f81ba19..2d88bd8a8fbe62c6783e00bd351e26ebd3cde80c 100644
--- a/frontend/src/metabase/questions/containers/Archive.jsx
+++ b/frontend/src/metabase/questions/containers/Archive.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import HeaderWithBack from "metabase/components/HeaderWithBack";
-import SearchHeader from "../components/SearchHeader";
+import SearchHeader from "metabase/components/SearchHeader";
 import ArchivedItem from "../components/ArchivedItem";
 
 import { loadEntities, setArchived, setSearchText } from "../questions";
diff --git a/frontend/src/metabase/questions/containers/EntityList.jsx b/frontend/src/metabase/questions/containers/EntityList.jsx
index 95227f8e7f69a60262b88891d4f2af13963dd58d..5ae812e30c76c147c07fb6a2a04e0366ad69b0ee 100644
--- a/frontend/src/metabase/questions/containers/EntityList.jsx
+++ b/frontend/src/metabase/questions/containers/EntityList.jsx
@@ -4,15 +4,14 @@ import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
 
-import Icon from "metabase/components/Icon";
 import EmptyState from "metabase/components/EmptyState";
-import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import FilterWidget from "metabase/components/FilterWidget"
 
 import S from "../components/List.css";
 
 import List from "../components/List";
-import SearchHeader from "../components/SearchHeader";
+import SearchHeader from "metabase/components/SearchHeader";
 import ActionHeader from "../components/ActionHeader";
 
 import _ from "underscore";
@@ -57,37 +56,37 @@ const mapDispatchToProps = {
 
 const SECTIONS = [
     {
-        section: 'all',
+        id: 'all',
         name: 'All questions',
         icon: 'all',
         empty: 'No questions have been saved yet.',
     },
     {
-        section: 'fav',
+        id: 'fav',
         name: 'Favorites',
         icon: 'star',
         empty: 'You haven\'t favorited any questions yet.',
     },
     {
-        section: 'recent',
+        id: 'recent',
         name: 'Recently viewed',
         icon: 'recents',
         empty: 'You haven\'t viewed any questions recently.',
     },
     {
-        section: 'mine',
+        id: 'mine',
         name: 'Saved by me',
         icon: 'mine',
         empty:  'You haven\'t saved any questions yet.'
     },
     {
-        section: 'popular',
+        id: 'popular',
         name: 'Most popular',
         icon: 'popular',
         empty: 'The most viewed questions across your company will show up here.',
     },
     {
-        section: 'archived',
+        id: 'archived',
         name: "Archive",
         icon: 'archive',
         empty: 'If you no longer need a question, you can archive it.'
@@ -159,7 +158,7 @@ export default class EntityList extends Component {
     }
 
     getSection () {
-        return _.findWhere(SECTIONS, { section: this.props.entityQuery && this.props.entityQuery.f || "all" }) || DEFAULT_SECTION;
+        return _.findWhere(SECTIONS, { id: this.props.entityQuery && this.props.entityQuery.f || "all" }) || DEFAULT_SECTION;
     }
 
     render() {
@@ -196,17 +195,20 @@ export default class EntityList extends Component {
                                 labels={labels}
                             />
                         : showSearchHeader ?
-                            <SearchHeader
-                                searchText={searchText}
-                                setSearchText={setSearchText}
-                            />
+                                <div style={{marginLeft: "10px"}}>
+                                    <SearchHeader
+                                        searchText={searchText}
+                                        setSearchText={setSearchText}
+                                    />
+                                </div>
                         :
                             null
                       }
                       { showEntityFilterWidget && hasEntitiesInPlainState &&
-                          <EntityFilterWidget
-                            section={section}
-                            onChange={onChangeSection}
+                          <FilterWidget
+                              items={SECTIONS.filter(item => item.id !== "archived")}
+                              activeItem={section}
+                              onChange={(item) => onChangeSection(item.id)}
                           />
                       }
                     </div>
@@ -224,7 +226,7 @@ export default class EntityList extends Component {
                         />
                     :
                         <div className={S.empty}>
-                            <EmptyState message={section.section === "all" && this.props.defaultEmptyState ? this.props.defaultEmptyState : section.empty} icon={section.icon} />
+                            <EmptyState message={section.id === "all" && this.props.defaultEmptyState ? this.props.defaultEmptyState : section.empty} icon={section.icon} />
                         </div>
                 }
                 </LoadingAndErrorWrapper>
@@ -232,54 +234,3 @@ export default class EntityList extends Component {
         );
     }
 }
-
-class EntityFilterWidget extends Component {
-    static propTypes = {
-        section: PropTypes.object.isRequired,
-        onChange: PropTypes.func.isRequired,
-    }
-    render() {
-        const { section, onChange } = this.props;
-        return (
-            <PopoverWithTrigger
-                ref={p => this.popover = p}
-                triggerClasses="block ml-auto flex-no-shrink"
-                targetOffsetY={10}
-                triggerElement={
-                    <div className="ml2 flex align-center text-brand">
-                        <span className="text-bold">{section && section.name}</span>
-                        <Icon
-                            ref={i => this.icon = i}
-                            className="ml1"
-                            name="chevrondown"
-                            width="12"
-                            height="12"
-                        />
-                    </div>
-                }
-                target={() => this.icon}
-            >
-                <ol className="text-brand mt2 mb1">
-                    { SECTIONS.filter(item => item.section !== "archived").map((item, index) =>
-                        <li
-                            key={index}
-                            className="cursor-pointer flex align-center brand-hover px2 py1 mb1"
-                            onClick={() => {
-                                onChange(item.section);
-                                this.popover.close();
-                            }}
-                        >
-                            <Icon
-                                className="mr1 text-light-blue"
-                                name={item.icon}
-                            />
-                            <h4 className="List-item-title">
-                                {item.name}
-                            </h4>
-                        </li>
-                    ) }
-                </ol>
-            </PopoverWithTrigger>
-        )
-    }
-}
diff --git a/frontend/src/metabase/questions/selectors.js b/frontend/src/metabase/questions/selectors.js
index 8710ce228e17af25be5bace0ad6f6300c95f2e33..ced278541ee04724e49ee195254a10379d068584 100644
--- a/frontend/src/metabase/questions/selectors.js
+++ b/frontend/src/metabase/questions/selectors.js
@@ -5,10 +5,7 @@ import { getIn } from "icepick";
 import _ from "underscore";
 
 import visualizations from "metabase/visualizations";
-
-function caseInsensitiveSearch(haystack, needle) {
-    return !needle || (haystack != null && haystack.toLowerCase().indexOf(needle.toLowerCase()) >= 0);
-}
+import {caseInsensitiveSearch} from "metabase/lib/string"
 
 export const getEntityType          = (state, props) => props && props.entityType ? props.entityType : state.questions.lastEntityType;
 export const getEntityQuery         = (state, props) => props && props.entityQuery ? JSON.stringify(props.entityQuery) : state.questions.lastEntityQuery;
diff --git a/frontend/src/metabase/reducers-main.js b/frontend/src/metabase/reducers-main.js
index f4b176268a108ea4e6c658c063b091db2aca6d8f..52575d9167c17a48d6926ba584556f155728b172 100644
--- a/frontend/src/metabase/reducers-main.js
+++ b/frontend/src/metabase/reducers-main.js
@@ -7,7 +7,7 @@ import { combineReducers } from 'redux';
 import commonReducers from "./reducers-common";
 
 /* admin */
-import adminReducers from "./reducers-admin";
+import admin from "metabase/admin/admin";
 
 /* setup */
 import * as setup from "metabase/setup/reducers";
@@ -16,6 +16,7 @@ import * as setup from "metabase/setup/reducers";
 import * as user from "metabase/user/reducers";
 
 /* dashboards */
+import dashboards from "metabase/dashboards/dashboards";
 import dashboard from "metabase/dashboard/dashboard";
 import * as home from "metabase/home/reducers";
 
@@ -35,6 +36,7 @@ export default {
     ...commonReducers,
 
     // main app reducers
+    dashboards,
     dashboard,
     home: combineReducers(home),
     pulse: combineReducers(pulse),
@@ -45,6 +47,5 @@ export default {
     reference,
     setup: combineReducers(setup),
     user: combineReducers(user),
-
-    ...adminReducers
+    admin
 };
diff --git a/frontend/src/metabase/redux/metadata.js b/frontend/src/metabase/redux/metadata.js
index 553850a080fa3f6092fe30438889a54d5a316460..94f774b315b8c3666bda404b031da4cfc6f7554d 100644
--- a/frontend/src/metabase/redux/metadata.js
+++ b/frontend/src/metabase/redux/metadata.js
@@ -1,6 +1,7 @@
 import {
     handleActions,
     combineReducers,
+    createAction,
     createThunkAction,
     resourceListToMap,
     fetchData,
@@ -320,6 +321,24 @@ const tables = handleActions({
     [FETCH_DATABASE_METADATA]: { next: (state, { payload }) => ({ ...state, ...payload.tables }) }
 }, {});
 
+const FETCH_FIELD_VALUES = "metabase/metadata/FETCH_FIELD_VALUES";
+export const fetchFieldValues = createThunkAction(FETCH_FIELD_VALUES, function(fieldId, reload) {
+    return async function(dispatch, getState) {
+        const requestStatePath = ["metadata", "fields", fieldId];
+        const existingStatePath = requestStatePath;
+        const getData = () => MetabaseApi.field_values({ fieldId })
+
+        return await fetchData({
+            dispatch,
+            getState,
+            requestStatePath,
+            existingStatePath,
+            getData,
+            reload
+        });
+    };
+});
+
 const UPDATE_FIELD = "metabase/metadata/UPDATE_FIELD";
 export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
     return async function(dispatch, getState) {
@@ -349,21 +368,18 @@ export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
     };
 });
 
+export const ADD_PARAM_VALUES = "metabase/metadata/ADD_PARAM_VALUES";
+export const addParamValues = createAction(ADD_PARAM_VALUES);
+
 const fields = handleActions({
     [FETCH_TABLE_METADATA]: { next: (state, { payload }) => ({ ...state, ...payload.fields }) },
     [FETCH_DATABASE_METADATA]: { next: (state, { payload }) => ({ ...state, ...payload.fields }) },
     [UPDATE_FIELD]: { next: (state, { payload }) => payload },
-    // NOTE: from metabase/dashboard/dashboard
-    ["metabase/dashboard/FETCH_DASHBOARD"]: { next: (state, { payload }) => {
-        // extract field values from dashboard endpoint
-        if (payload.entities && payload.entities.dashboard) {
-            for (const dashboard of Object.values(payload.entities.dashboard)) {
-                if (dashboard.param_values) {
-                    for (const fieldValues of Object.values(dashboard.param_values)) {
-                        state = assocIn(state, [fieldValues.field_id, "values"], fieldValues);
-                    }
-                }
-            }
+    [FETCH_FIELD_VALUES]: { next: (state, { payload: fieldValues }) =>
+        fieldValues ? assocIn(state, [fieldValues.field_id, "values"], fieldValues) : state },
+    [ADD_PARAM_VALUES]: { next: (state, { payload: paramValues }) => {
+        for (const fieldValues of Object.values(paramValues)) {
+            state = assocIn(state, [fieldValues.field_id, "values"], fieldValues);
         }
         return state;
     }}
diff --git a/frontend/src/metabase/reference/containers/ReferenceApp.jsx b/frontend/src/metabase/reference/containers/ReferenceApp.jsx
index dbb904d0a65fc2e7d2e8b5a85799b7aef51a349a..de4bec50e79ccc36883d2c535ba6366c50e8aac8 100644
--- a/frontend/src/metabase/reference/containers/ReferenceApp.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceApp.jsx
@@ -28,7 +28,7 @@ import {
 
 import {
     fetchDashboards
-} from 'metabase/dashboard/dashboard';
+} from 'metabase/dashboards/dashboards';
 
 const mapStateToProps = (state, props) => ({
     sectionId: getSectionId(state, props),
diff --git a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
index b10985db50ea0ee655ab705573c0ee36f8cc706c..c5218c5073f1a365f89268a90143edf7f4d86721 100644
--- a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
@@ -3,15 +3,11 @@ 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';
 import Modal from 'metabase/components/Modal.jsx';
@@ -25,10 +21,7 @@ import GuideDetailEditor from "metabase/reference/components/GuideDetailEditor.j
 import * as metadataActions from 'metabase/redux/metadata';
 import * as actions from 'metabase/reference/reference';
 import { clearRequestState } from "metabase/redux/requests";
-import {
-    updateDashboard,
-    createDashboard
-} from 'metabase/dashboard/dashboard';
+import { createDashboard, updateDashboard } from 'metabase/dashboards/dashboards';
 
 import {
     updateSetting
@@ -73,7 +66,7 @@ const mapStateToProps = (state, props) => {
     const initialValues = guide && {
         things_to_know: guide.things_to_know || null,
         contact: guide.contact || {name: null, email: null},
-        most_important_dashboard: guide.most_important_dashboard !== null ?
+        most_important_dashboard: dashboards !== null && guide.most_important_dashboard !== null ?
             dashboards[guide.most_important_dashboard] :
             {},
         important_metrics: guide.important_metrics && guide.important_metrics.length > 0 ?
@@ -116,7 +109,6 @@ const mapStateToProps = (state, props) => {
 };
 
 const mapDispatchToProps = {
-    push,
     updateDashboard,
     createDashboard,
     updateSetting,
@@ -170,7 +162,6 @@ export default class ReferenceGettingStartedGuide extends Component {
         isDashboardModalOpen: PropTypes.bool,
         showDashboardModal: PropTypes.func,
         hideDashboardModal: PropTypes.func,
-        push: PropTypes.func
     };
 
     render() {
@@ -204,7 +195,6 @@ export default class ReferenceGettingStartedGuide extends Component {
             isDashboardModalOpen,
             showDashboardModal,
             hideDashboardModal,
-            push
         } = this.props;
 
         const onSubmit = handleSubmit(async (fields) =>
@@ -227,14 +217,11 @@ export default class ReferenceGettingStartedGuide extends Component {
                         <CreateDashboardModal
                             createDashboardFn={async (newDashboard) => {
                                 try {
-                                    const action = await createDashboard(newDashboard, true);
-                                    push(Urls.dashboard(action.payload.id));
+                                    await createDashboard(newDashboard, { redirect: true });
                                 }
                                 catch(error) {
                                     console.error(error);
                                 }
-
-                                MetabaseAnalytics.trackEvent("Dashboard", "Create");
                             }}
                             onClose={hideDashboardModal}
                         />
diff --git a/frontend/src/metabase/reference/selectors.js b/frontend/src/metabase/reference/selectors.js
index 5d4de8d7ab45d135cb292407334d3b9b514eecb8..88cb5aeb5040dd21e175d971710111d2e2c6ce72 100644
--- a/frontend/src/metabase/reference/selectors.js
+++ b/frontend/src/metabase/reference/selectors.js
@@ -1,5 +1,6 @@
 import { createSelector } from 'reselect';
 import { assoc, getIn } from "icepick";
+import { getDashboardListing } from "../dashboards/selectors";
 
 import Query, { AggregationClause } from 'metabase/lib/query';
 import {
@@ -15,6 +16,7 @@ import {
 
 import _ from "underscore";
 
+
 // there might be a better way to organize sections
 // it feels like I'm duplicating a lot of routing logic here
 //TODO: refactor to use different container components for each section
@@ -697,7 +699,6 @@ export const getIsFormulaExpanded = (state, props) => state.reference.isFormulaE
 
 export const getGuide = (state, props) => state.reference.guide;
 
-export const getDashboards = (state, props) => state.dashboard.dashboardListing &&
-    resourceListToMap(state.dashboard.dashboardListing);
+export const getDashboards = (state, props) => getDashboardListing(state) && resourceListToMap(getDashboardListing(state));
 
 export const getIsDashboardModalOpen = (state, props) => state.reference.isDashboardModalOpen;
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index fc9ae811192016a2a0cbb2a8721b1697569c09ca..af2c28174fab2a8cfaa76c881a1b7459fbbf9b33 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -20,8 +20,9 @@ import PasswordResetApp from "metabase/auth/containers/PasswordResetApp.jsx";
 import GoogleNoAccount from "metabase/auth/components/GoogleNoAccount.jsx";
 
 // main app containers
-import DashboardApp from "metabase/dashboard/containers/DashboardApp.jsx";
 import HomepageApp from "metabase/home/containers/HomepageApp.jsx";
+import Dashboards from "metabase/dashboards/containers/Dashboards.jsx";
+import DashboardApp from "metabase/dashboard/containers/DashboardApp.jsx";
 
 import QuestionIndex from "metabase/questions/containers/QuestionIndex.jsx";
 import Archive from "metabase/questions/containers/Archive.jsx";
@@ -152,7 +153,10 @@ export const getRoutes = (store) =>
                 {/* HOME */}
                 <Route path="/" component={HomepageApp} />
 
-                {/* DASHBOARD */}
+                {/* DASHBOARD LIST */}
+                <Route path="/dashboard" title="Dashboards" component={Dashboards} />
+
+                {/* INDIVIDUAL DASHBOARDS */}
                 <Route path="/dashboard/:dashboardId" title="Dashboard" component={DashboardApp} />
 
                 {/* QUERY BUILDER */}
@@ -272,8 +276,9 @@ export const getRoutes = (store) =>
             </Route>
 
             {/* DEPRECATED */}
-            <Redirect from="/q" to="/question" />
-            <Redirect from="/card/:cardId" to="/question/:cardId" />
+            {/* NOTE: these custom routes are needed because <Redirect> doesn't preserve the hash */}
+            <Route path="/q" onEnter={({ location }, replace) => replace({ pathname: "/question", hash: location.hash })} />
+            <Route path="/card/:cardId" onEnter={({ location, params }, replace) => replace({ pathname: `/question/${params.cardId}`, hash: location.hash })} />
             <Redirect from="/dash/:dashboardId" to="/dashboard/:dashboardId" />
 
             {/* MISC */}
diff --git a/frontend/src/metabase/selectors/metadata.js b/frontend/src/metabase/selectors/metadata.js
index 744a84b3c609fc1c7d02a192457de3eb15be5315..9c0a1b9c5d14bf32a4a64380814d0fac7f0864a2 100644
--- a/frontend/src/metabase/selectors/metadata.js
+++ b/frontend/src/metabase/selectors/metadata.js
@@ -1,5 +1,11 @@
+import { getIn } from "icepick";
+import { getFieldValues } from "metabase/lib/query/field";
 
 export const getTables = (state) => state.metadata.tables;
 export const getFields = (state) => state.metadata.fields;
 export const getMetrics = (state) => state.metadata.metrics;
 export const getDatabases = (state) => Object.values(state.metadata.databases);
+
+export const getParameterFieldValues = (state, props) => {
+    return getFieldValues(getIn(state, ["metadata", "fields", props.parameter.field_id, "values"]));
+}
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 23cf9cc8fcdad1fe17abf96ebaa012111aa1bd0c..3fedea63d7a4127db49bbec1cbfd5696f70f57e9 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -119,7 +119,7 @@ export const MetabaseApi = {
     // table_sync_metadata:        POST("/api/table/:tableId/sync"),
     // field_get:                   GET("/api/field/:fieldId"),
     // field_summary:               GET("/api/field/:fieldId/summary"),
-    // field_values:                GET("/api/field/:fieldId/values"),
+    field_values:                GET("/api/field/:fieldId/values"),
     // field_value_map_update:     POST("/api/field/:fieldId/value_map_update"),
     field_update:                PUT("/api/field/:id"),
     dataset:                    POST("/api/dataset"),
diff --git a/frontend/src/metabase/tutorial/PageFlag.css b/frontend/src/metabase/tutorial/PageFlag.css
index 17819bb7d176192d0dd9516185a7146d93d54e94..42cfe58787659cec6ee4da31d91ff017ffea7346 100644
--- a/frontend/src/metabase/tutorial/PageFlag.css
+++ b/frontend/src/metabase/tutorial/PageFlag.css
@@ -19,6 +19,8 @@
   line-height: 24px;
 
   transition: left 0.5s ease-in-out, top 0.5s ease-in-out;
+
+  z-index: 5;
 }
 
 .PageFlag:before,
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
index 07bcaa255b0b28fc887723c29a5db3a7077bbdb2..d388dfcec98a16d474d28e8b6b51051b30e1122d 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
@@ -56,8 +56,12 @@ export default class LineAreaBarChart extends Component<*, VisualizationProps, *
         return getChartTypeFromData(cols, rows, false) != null;
     }
 
-    static checkRenderable([{ data: { cols, rows} }], settings) {
-        if (rows.length < 1) { throw new MinRowsError(1, rows.length); }
+    static checkRenderable(series, settings) {
+        const singleSeriesHasNoRows = ({ data: { cols, rows} }) => rows.length < 1;
+        if (_.every(series, singleSeriesHasNoRows)) {
+             throw new MinRowsError(1, 0);
+        }
+
         const dimensions = (settings["graph.dimensions"] || []).filter(name => name);
         const metrics = (settings["graph.metrics"] || []).filter(name => name);
         if (dimensions.length < 1 || metrics.length < 1) {
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index a7e0b67dce34a341e7b8e0576876c636bc7aa49c..5a49bb9d2741553a10c6f001e3b81c0cfef45f62 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -275,7 +275,7 @@ export default class Visualization extends Component<*, Props, State> {
 
         if (!error) {
             // $FlowFixMe
-            noResults = series[0] && series[0].data && datasetContainsNoResults(series[0].data);
+            noResults = _.every(series, s => s && s.data && datasetContainsNoResults(s.data));
         }
 
         let extra = (
diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
index a2ecd311f47ef29c49de0db216ad4f33c9701925..b5dc4bd92fb3403cfe4a89c782d469f5e9e77149 100644
--- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
+++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
@@ -33,6 +33,10 @@ import { determineSeriesIndexFromElement } from "./tooltip";
 import { formatValue } from "metabase/lib/formatting";
 import { parseTimestamp } from "metabase/lib/time";
 
+import { datasetContainsNoResults } from "metabase/lib/dataset";
+
+import type { Series } from "metabase/meta/types/Visualization"
+
 const MIN_PIXELS_PER_TICK = { x: 100, y: 32 };
 const BAR_PADDING_RATIO = 0.2;
 const DEFAULT_INTERPOLATION = "linear";
@@ -103,11 +107,15 @@ function initChart(chart, element) {
 }
 
 function applyChartTimeseriesXAxis(chart, settings, series, xValues, xDomain, xInterval) {
+    // find the first nonempty single series
+    // $FlowFixMe
+    const firstSeries: Series = _.find(series, (s) => !datasetContainsNoResults(s.data));
+
     // setup an x-axis where the dimension is a timeseries
-    let dimensionColumn = series[0].data.cols[0];
+    let dimensionColumn = firstSeries.data.cols[0];
 
     // get the data's timezone offset from the first row
-    let dataOffset = parseTimestamp(series[0].data.rows[0][0]).utcOffset() / 60;
+    let dataOffset = parseTimestamp(firstSeries.data.rows[0][0]).utcOffset() / 60;
 
     // compute the data interval
     let dataInterval = xInterval;
@@ -149,7 +157,10 @@ function applyChartTimeseriesXAxis(chart, settings, series, xValues, xDomain, xI
 }
 
 function applyChartQuantitativeXAxis(chart, settings, series, xValues, xDomain, xInterval) {
-    const dimensionColumn = series[0].data.cols[0];
+    // find the first nonempty single series
+    // $FlowFixMe
+    const firstSeries: Series = _.find(series, (s) => !datasetContainsNoResults(s.data));
+    const dimensionColumn = firstSeries.data.cols[0];
 
     if (settings["graph.x_axis.labels_enabled"]) {
         chart.xAxisLabel(settings["graph.x_axis.title_text"] || getFriendlyName(dimensionColumn), X_LABEL_PADDING);
@@ -187,7 +198,11 @@ function applyChartQuantitativeXAxis(chart, settings, series, xValues, xDomain,
 }
 
 function applyChartOrdinalXAxis(chart, settings, series, xValues) {
-    const dimensionColumn = series[0].data.cols[0];
+    // find the first nonempty single series
+    // $FlowFixMe
+    const firstSeries: Series = _.find(series, (s) => !datasetContainsNoResults(s.data));
+
+    const dimensionColumn = firstSeries.data.cols[0];
 
     if (settings["graph.x_axis.labels_enabled"]) {
         chart.xAxisLabel(settings["graph.x_axis.title_text"] || getFriendlyName(dimensionColumn), X_LABEL_PADDING);
@@ -812,10 +827,14 @@ export default function lineAreaBar(element, { series, onHoverChange, onVisualiz
     const isQuantitative = ["linear", "log", "pow"].indexOf(settings["graph.x_axis.scale"]) >= 0;
     const isOrdinal = !isTimeseries && !isQuantitative;
 
-    const isDimensionTimeseries = dimensionIsTimeseries(series[0].data);
-    const isDimensionNumeric = dimensionIsNumeric(series[0].data);
+    // find the first nonempty single series
+    // $FlowFixMe
+    const firstSeries: Series = _.find(series, (s) => !datasetContainsNoResults(s.data));
+
+    const isDimensionTimeseries = dimensionIsTimeseries(firstSeries.data);
+    const isDimensionNumeric = dimensionIsNumeric(firstSeries.data);
 
-    if (series[0].data.cols.length < 2) {
+    if (firstSeries.data.cols.length < 2) {
         throw new Error("This chart type requires at least 2 columns.");
     }
 
@@ -955,7 +974,11 @@ export default function lineAreaBar(element, { series, onHoverChange, onVisualiz
 
         dimension = dataset.dimension(d => d[0]);
         groups = datas.map((data, seriesIndex) => {
+            // If the value is empty, pass a dummy array to crossfilter
+            data = data.length > 0 ? data : [[null, null]];
+
             let dim = crossfilter(data).dimension(d => d[0]);
+
             return data[0].slice(1).map((_, metricIndex) =>
                 reduceGroup(dim.group(), metricIndex + 1, () => warn(UNAGGREGATED_DATA_WARNING(series[seriesIndex].data.cols[0])))
             );
@@ -1048,7 +1071,8 @@ export default function lineAreaBar(element, { series, onHoverChange, onVisualiz
 
     let onGoalHover = () => {};
     if (settings["graph.show_goal"]) {
-        const goalData = [[xDomain[0], settings["graph.goal_value"]], [xDomain[1], settings["graph.goal_value"]]];
+        const goalValue = settings["graph.goal_value"];
+        const goalData = [[xDomain[0], goalValue], [xDomain[1], goalValue]];
         const goalDimension = crossfilter(goalData).dimension(d => d[0]);
         const goalGroup = goalDimension.group().reduceSum(d => d[1]);
         const goalIndex = charts.length;
@@ -1065,8 +1089,8 @@ export default function lineAreaBar(element, { series, onHoverChange, onVisualiz
 
         onGoalHover = (element) => {
             onHoverChange(element && {
-                element: element,
-                data: [{ key: "Goal", value: settings["graph.goal"] }]
+                element,
+                data: [{ key: "Goal", value: goalValue }]
             });
         }
     }
diff --git a/frontend/test/e2e/dashboard/dashboard.spec.js b/frontend/test/e2e/dashboard/dashboard.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7731cbcc500de8c617eee51e406313043e4da739
--- /dev/null
+++ b/frontend/test/e2e/dashboard/dashboard.spec.js
@@ -0,0 +1,56 @@
+import {
+    ensureLoggedIn,
+    describeE2E
+} from "../support/utils";
+
+import {createDashboardInEmptyState} from "../dashboards/dashboards.utils"
+import {removeCurrentDash} from "../dashboard/dashboard.utils"
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
+
+const EDIT_DASHBOARD_SELECTOR = ".Icon.Icon-pencil";
+const SAVE_DASHBOARD_SELECTOR = ".EditHeader .flex-align-right .Button--primary.Button";
+
+describeE2E("dashboards/dashboards", () => {
+    beforeEach(async () => {
+        await ensureLoggedIn(server, driver, "bob@metabase.com", "12341234");
+    });
+
+    describe("dashboards list", () => {
+        xit("should let you create new dashboards, see them, filter them and enter them", async () => {
+            // Delegate dashboard creation to dashboard list test code
+            await createDashboardInEmptyState();
+
+            // Test dashboard renaming
+            await d.select(EDIT_DASHBOARD_SELECTOR).wait().click();
+            await d.select(".Header-title > input:nth-of-type(1)").wait().clear().sendKeys("Customer Analysis Paralysis");
+            await d.select(".Header-title > input:nth-of-type(2)").wait().sendKeys(""); // Test empty description
+
+            await d.select(SAVE_DASHBOARD_SELECTOR).wait().click();
+            await d.select(".DashboardHeader h2:contains(Paralysis)").wait();
+
+            // Test parameter filter creation
+            await d.select(EDIT_DASHBOARD_SELECTOR).wait().click();
+            await d.select(".Icon.Icon-funneladd").wait().click();
+
+            // TODO: After `annotate-react-dom` supports functional components in production builds, use this instead:
+            // `await d.select(":react(ParameterOptionsSection):contains(Time)").wait().click();`
+            await d.select(".PopoverBody--withArrow li > div:contains(Time)").wait().click();
+
+            // TODO: Replace when possible with `await d.select(":react(ParameterOptionItem):contains(Relative)").wait().click()`;
+            await d.select(".PopoverBody--withArrow li > div:contains(Relative)").wait().click(); // Relative date
+
+            await d.select(":react(ParameterValueWidget)").wait().click();
+            await d.select(":react(PredefinedRelativeDatePicker) button:contains(Yesterday)").wait().click();
+            expect(await d.select(":react(ParameterValueWidget) .text-nowrap").wait().text()).toEqual("Yesterday");
+
+            // TODO: Replace when possible with `await d.select(":react(HeaderModal) button:contains(Done)").wait().click();`
+            await d.select(".absolute.top.left.right button:contains(Done)").wait().click();
+            // Wait until the header modal exit animation is finished
+            await d.sleep(1000);
+            // Remove the created dashboards to prevent clashes with other tests
+            await removeCurrentDash();
+        });
+
+    });
+});
diff --git a/frontend/test/e2e/dashboard/dashboard.utils.js b/frontend/test/e2e/dashboard/dashboard.utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ff3e98379884df4ff0ac6c2b5c753c274bdc238
--- /dev/null
+++ b/frontend/test/e2e/dashboard/dashboard.utils.js
@@ -0,0 +1,5 @@
+export const removeCurrentDash = async () => {
+    await d.select(".Icon.Icon-pencil").wait().click();
+    await d.select(".EditHeader .flex-align-right a:nth-of-type(2)").wait().click();
+    await d.select(".Button.Button--danger").wait().click();
+}
diff --git a/frontend/test/e2e/dashboards/dashboards.spec.js b/frontend/test/e2e/dashboards/dashboards.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b938c85fde9e5b68967e0d2ebba4712176873e8
--- /dev/null
+++ b/frontend/test/e2e/dashboards/dashboards.spec.js
@@ -0,0 +1,58 @@
+import {
+    ensureLoggedIn,
+    describeE2E
+} from "../support/utils";
+
+import {
+    createDashboardInEmptyState, getLatestDashboardUrl, getPreviousDashboardUrl,
+    incrementDashboardCount
+} from "./dashboards.utils"
+import {removeCurrentDash} from "../dashboard/dashboard.utils"
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
+
+describeE2E("dashboards/dashboards", () => {
+    describe("dashboards list", () => {
+        beforeEach(async () => {
+            await ensureLoggedIn(server, driver, "bob@metabase.com", "12341234");
+        });
+
+        xit("should let you create new dashboards, see them, filter them and enter them", async () => {
+            await d.get("/dashboard");
+            await d.screenshot("screenshots/dashboards.png");
+
+            await createDashboardInEmptyState();
+
+            // Return to the dashboard list and re-enter the card through the list item
+            await driver.get(`${server.host}/dashboard`);
+            await d.select(".Grid-cell > a").wait().click();
+            await d.waitUrl(getLatestDashboardUrl());
+
+            // Create another one
+            await d.get(`${server.host}/dashboard`);
+            await d.select(".Icon.Icon-add").wait().click();
+            await d.select("#CreateDashboardModal input[name='name']").wait().sendKeys("Some Excessively Long Dashboard Title Just For Fun");
+            await d.select("#CreateDashboardModal input[name='description']").wait().sendKeys("");
+            await d.select("#CreateDashboardModal .Button--primary").wait().click();
+            incrementDashboardCount();
+            await d.waitUrl(getLatestDashboardUrl());
+
+            // Test filtering
+            await d.get(`${server.host}/dashboard`);
+            await d.select("input[type='text']").wait().sendKeys("this should produce no results");
+            await d.select("img[src*='empty_dashboard']");
+
+            // Should search from both title and description
+            await d.select("input[type='text']").wait().clear().sendKeys("usual response times");
+            await d.select(".Grid-cell > a").wait().click();
+            await d.waitUrl(getPreviousDashboardUrl(1));
+
+            // Remove the created dashboards to prevent clashes with other tests
+            await removeCurrentDash();
+            // Should return to dashboard page where only one dash left
+            await d.select(".Grid-cell > a").wait().click();
+            await removeCurrentDash();
+        });
+
+    });
+});
diff --git a/frontend/test/e2e/dashboards/dashboards.utils.js b/frontend/test/e2e/dashboards/dashboards.utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..b19c207236572365e98ab8be303751f46728eae2
--- /dev/null
+++ b/frontend/test/e2e/dashboards/dashboards.utils.js
@@ -0,0 +1,25 @@
+export var dashboardCount = 0
+export const incrementDashboardCount = () => {
+    dashboardCount += 1;
+}
+export const getLatestDashboardUrl = () => {
+    console.log(`/dashboard/${dashboardCount}`)
+    return `/dashboard/${dashboardCount}`
+}
+export const getPreviousDashboardUrl = (nFromLatest) => {
+    return `/dashboard/${dashboardCount - nFromLatest}`
+}
+
+export const createDashboardInEmptyState = async () => {
+    await d.get("/dashboard");
+
+    // Create a new dashboard in the empty state (EmptyState react component)
+    await d.select(".Button.Button--primary").wait().click();
+    await d.select("#CreateDashboardModal input[name='name']").wait().sendKeys("Customer Feedback Analysis");
+    await d.select("#CreateDashboardModal input[name='description']").wait().sendKeys("For seeing the usual response times, feedback topics, our response rate, how often customers are directed to our knowledge base instead of providing a customized response");
+    await d.select("#CreateDashboardModal .Button--primary").wait().click();
+
+    incrementDashboardCount();
+    await d.waitUrl(getLatestDashboardUrl());
+
+}
diff --git a/frontend/test/e2e/parameters/questions.spec.js b/frontend/test/e2e/parameters/questions.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a66f5a2d2cec4ca89157c6cc782e957c6fe13ae
--- /dev/null
+++ b/frontend/test/e2e/parameters/questions.spec.js
@@ -0,0 +1,131 @@
+
+import {
+    describeE2E,
+    ensureLoggedIn
+} from "../support/utils";
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
+
+import { startNativeQuestion, saveQuestion, logout } from "../support/metabase";
+
+async function setCategoryParameter(value) {
+    // currently just selects the first parameter
+    await this.select(":react(Parameters) a").wait().click()
+    await this.select(":react(CategoryWidget) li:contains(" + value + ")").wait().click();
+    return this;
+}
+
+async function checkScalar(value) {
+    await this.sleep(250);
+    await this.select(".ScalarValue :react(Scalar)").waitText(value);
+    return this;
+}
+
+const COUNT_ALL = "200";
+const COUNT_DOOHICKEY = "56";
+const COUNT_GADGET = "43";
+
+describeE2E("parameters", () => {
+    beforeEach(async () => {
+        await ensureLoggedIn(server, driver, "bob@metabase.com", "12341234");
+    });
+
+    describe("questions", () => {
+        it("should allow users to enable public sharing", async () => {
+            // load public sharing settings
+            await d.get("/admin/settings/public_sharing");
+            // if enabled, disable it so we're in a known state
+            if ((await d.select(":react(SettingsSetting) .flex .text-bold").wait().text()) === "Enabled") {
+                await d.select(":react(SettingsSetting) :react(Toggle)").wait().click();
+            }
+            // toggle it on
+            await d.select(":react(SettingsSetting) :react(Toggle)").wait().click();
+            // make sure it's enabled
+            await d.select(":react(SettingsSetting) .flex .text-bold").waitText("Enabled");
+        })
+        it("should allow users to enable embedding", async () => {
+            // load embedding settings
+            await d.get("/admin/settings/embedding_in_other_applications");
+            try {
+                // if enabled, disable it so we're in a known state
+                await d.select(":react(Toggle)").wait().click();
+            } catch (e) {
+            }
+            // enable it
+            await d.select(".Button:contains(Enable)").wait().click();
+            // make sure it's enabled
+            await d.select(":react(SettingsSetting) .flex .text-bold").waitText("Enabled");
+        });
+        it("should allow users to create parameterized SQL questions", async () => {
+            await d::startNativeQuestion("select count(*) from products where {{category}}")
+
+            await d.sleep(500);
+            await d.select(".ColumnarSelector-row:contains(Field)").wait().click();
+            await d.select(".PopoverBody .AdminSelect").wait().sendKeys("cat");
+            await d.select(".ColumnarSelector-row:contains(Category)").wait().click();
+
+            // test without the parameter
+            await d.select(".RunButton").wait().click();
+            await d::checkScalar(COUNT_ALL);
+
+            // test the parameter
+            await d::setCategoryParameter("Doohickey");
+            await d.select(".RunButton").wait().click();
+            await d::checkScalar(COUNT_DOOHICKEY);
+
+            // save the question, required for public link/embedding
+            await d::saveQuestion("sql parameterized");
+
+            // open sharing panel
+            await d.select(".Icon-share").wait().click();
+
+            // open application embedding panel
+            await d.select(":react(SharingPane) .text-purple:contains(Embed)").wait().click();
+            // make the parameter editable
+            await d.select(".AdminSelect-content:contains(Disabled)").wait().click();
+            await d.select(":react(Option):contains(Editable)").wait().click();
+            await d.sleep(500);
+            // publish
+            await d.select(".Button:contains(Publish)").wait().click();
+
+            // get the embed URL
+            const embedUrl = (await d.select(":react(PreviewPane) iframe").wait().attribute("src")).replace(/#.*$/, "");
+
+            // back to main share panel
+            await d.select("h2 a span:contains(Sharing)").wait().click();
+
+            // toggle public link on
+            await d.select(":react(SharingPane) :react(Toggle)").wait().click();
+
+            // get the public URL
+            const publicUrl = (await d.select(":react(CopyWidget) input").wait().attribute("value")).replace(/#.*$/, "");
+
+            // logout to ensure it works for non-logged in users
+            d::logout();
+
+            // public url
+            await d.get(publicUrl);
+            await d::checkScalar(COUNT_ALL);
+
+            // manually click parameter
+            await d::setCategoryParameter("Doohickey");
+            await d::checkScalar(COUNT_DOOHICKEY);
+
+            // set parameter via url
+            await d.get(publicUrl + "?category=Gadget");
+            await d::checkScalar(COUNT_GADGET);
+
+            // embed
+            await d.get(embedUrl);
+            await d::checkScalar(COUNT_ALL);
+
+            // manually click parameter
+            await d::setCategoryParameter("Doohickey");
+            await d::checkScalar(COUNT_DOOHICKEY);
+
+            // set parameter via url
+            await d.get(embedUrl + "?category=Gadget");
+            await d::checkScalar(COUNT_GADGET);
+        });
+    });
+});
diff --git a/frontend/test/e2e/query_builder/query_builder.spec.js b/frontend/test/e2e/query_builder/query_builder.spec.js
index 9eb6aa8d69d39a6dc0d9d572ca0f2113db7db8ee..616170c39c10eb1777c9a3d996b14fb014c67db6 100644
--- a/frontend/test/e2e/query_builder/query_builder.spec.js
+++ b/frontend/test/e2e/query_builder/query_builder.spec.js
@@ -1,10 +1,14 @@
-
 import {
-    screenshot,
     describeE2E,
     ensureLoggedIn
 } from "../support/utils";
 
+import {removeCurrentDash} from "../dashboard/dashboard.utils";
+import {
+    createDashboardInEmptyState, getLatestDashboardUrl,
+    incrementDashboardCount
+} from "../dashboards/dashboards.utils";
+
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
 
 describeE2E("query_builder", () => {
@@ -23,18 +27,19 @@ describeE2E("query_builder", () => {
 
             await d.select(":react(AggregationWidget)").wait().click();
 
-            await d.select("#AggregationPopover .List-item:nth-child(2)>a").wait().click();
+            await d.select("#AggregationPopover .List-item a:contains(Count)").wait().click();
 
             await d.select(".Query-section.Query-section-breakout #BreakoutWidget").wait().click();
-            await d.select("#BreakoutPopover .List-section:nth-child(3) .List-section-header").wait().click();
-            await d.select("#BreakoutPopover .List-item:nth-child(12)>a").wait().click();
+            await d.select("#BreakoutPopover .List-section .List-section-header:contains(Product)").wait().click();
+            await d.select("#BreakoutPopover .List-item a:contains(Category)").wait().click();
 
             await d.select(".Query-section.Query-section-breakout #BreakoutWidget .AddButton").wait().click();
-            await d.select("#BreakoutPopover .List-item:first-child .Field-extra>a").wait().click();
-            await d.select("#TimeGroupingPopover .List-item:nth-child(4)>a").wait().click();
+            await d.select("#BreakoutPopover .List-item:contains(Created) .Field-extra > a").wait().click();
+            await d.select("#TimeGroupingPopover .List-item a:contains(Year)").wait().click();
 
             await d.select(".Button.RunButton").wait().click();
 
+            await d.sleep(500);
             await d.select(".Loading").waitRemoved(20000);
             await d.screenshot("screenshots/qb-pivot-table.png");
 
@@ -45,17 +50,33 @@ describeE2E("query_builder", () => {
 
             // add to new dashboard
             await d.select("#QuestionSavedModal .Button.Button--primary").wait().click();
+            try {
+                // this makes the test work wether we have any existing dashboards or not
+                await d.select("#AddToDashSelectDashModal h3:contains(Add)").wait(500).click();
+            } catch (e) {
+            }
+
+            // get the card ID from the URL
+            const cardId = (await d.wd().getCurrentUrl()).match(/\/question\/(\d+)/)[1];
+
             await d.select("#CreateDashboardModal input[name='name']").wait().sendKeys("Main Dashboard");
             await d.select("#CreateDashboardModal .Button.Button--primary").wait().click().waitRemoved(); // wait for the modal to be removed
+            incrementDashboardCount();
+
+            await d.waitUrl(getLatestDashboardUrl() + "?add=" + cardId);
 
             // save dashboard
             await d.select(".EditHeader .Button.Button--primary").wait().click();
             await d.select(".EditHeader").waitRemoved();
+
+            await removeCurrentDash();
         });
     });
 
     describe("charts", () => {
         xit("should allow users to create line charts", async () => {
+            await createDashboardInEmptyState();
+
             await d.get("/question");
 
             // select orders table
@@ -87,7 +108,7 @@ describeE2E("query_builder", () => {
             await d.sleep(500);
             await d.select("#VisualizationPopover li:nth-child(3)").wait().click();
 
-            await screenshot(driver, "screenshots/qb-line-chart.png");
+            await d.screenshot("screenshots/qb-line-chart.png");
 
             // save question
             await d.select(".Header-buttonSection:first-child").wait().click();
@@ -102,9 +123,13 @@ describeE2E("query_builder", () => {
             // save dashboard
             await d.select(".EditHeader .Button.Button--primary").wait().click();
             await d.select(".EditHeader").waitRemoved();
+
+            await removeCurrentDash();
         });
 
         xit("should allow users to create bar charts", async () => {
+            await createDashboardInEmptyState();
+
             // load line chart
             await d.get("/question/2");
 
@@ -127,7 +152,7 @@ describeE2E("query_builder", () => {
             await d.select(".Button.RunButton").wait().click();
             await d.select(".Loading").waitRemoved(20000);
 
-            await screenshot(driver, "screenshots/qb-bar-chart.png");
+            await d.screenshot("screenshots/qb-bar-chart.png");
 
             // save question
             await d.select(".Header-buttonSection:first-child").wait().click();
@@ -142,6 +167,8 @@ describeE2E("query_builder", () => {
             // save dashboard
             await d.select(".EditHeader .Button.Button--primary").wait().click();
             await d.select(".EditHeader").waitRemoved();
+
+            await removeCurrentDash();
         });
     });
 });
diff --git a/frontend/test/e2e/support/fixtures/metabase.db.h2.db b/frontend/test/e2e/support/fixtures/metabase.db.h2.db
index 6cda36762c740ce12cbc2047d0fd266b1c99ba19..525215b6019e31c1a16911b75bbb6753fa9b6f21 100644
Binary files a/frontend/test/e2e/support/fixtures/metabase.db.h2.db and b/frontend/test/e2e/support/fixtures/metabase.db.h2.db differ
diff --git a/frontend/test/e2e/support/metabase.js b/frontend/test/e2e/support/metabase.js
new file mode 100644
index 0000000000000000000000000000000000000000..53553043f6c8fa988b7bee867132820613d156ca
--- /dev/null
+++ b/frontend/test/e2e/support/metabase.js
@@ -0,0 +1,43 @@
+
+export async function logout() {
+    await this.wd().manage().deleteAllCookies();
+    return this;
+}
+
+export async function startGuiQuestion(text) {
+    await this.get("/question");
+    return this;
+}
+
+export async function startNativeQuestion(text) {
+    await this.get("/question");
+    await this.select(".Icon-sql").wait().click();
+    await this.select(".ace_text-input").wait().sendKeys(text);
+    return this;
+}
+
+export async function runQuery() {
+    await this.select(".RunButton").wait().click();
+    return this;
+}
+
+export async function saveQuestion(questionName, newDashboardName) {
+    // save question
+    await this.select(".Header-buttonSection:first-child").wait().click();
+    await this.select("#SaveQuestionModal input[name='name']").wait().sendKeys(questionName);
+    await this.select("#SaveQuestionModal .Button.Button--primary").wait().click().waitRemoved(); // wait for the modal to be removed
+
+    if (newDashboardName) {
+        // add to new dashboard
+        await this.select("#QuestionSavedModal .Button.Button--primary").wait().click();
+        await this.select("#CreateDashboardModal input[name='name']").wait().sendKeys(newDashboardName);
+        await this.select("#CreateDashboardModal .Button.Button--primary").wait().click().waitRemoved(); // wait for the modal to be removed
+    } else {
+        await this.select("#QuestionSavedModal .Button:contains(Not)").wait().click();
+    }
+
+    // wait for modal to close :-/
+    await this.sleep(500);
+
+    return this;
+}
diff --git a/frontend/test/e2e/support/sauce.js b/frontend/test/e2e/support/sauce.js
index 9090e094acd0f0e1ed728530f5462f059cc7703a..8aeebcd2abcae615f0b960c029e8e2e8c77e70bc 100644
--- a/frontend/test/e2e/support/sauce.js
+++ b/frontend/test/e2e/support/sauce.js
@@ -11,8 +11,8 @@ export const sauceServer = `http://${SAUCE_USERNAME}:${SAUCE_ACCESS_KEY}@localho
 
 export const sauceCapabilities = {
     browserName: 'chrome',
-    platform: 'Mac',
-    version: '48.0',
+    version: '52.0',
+    platform: 'macOS 10.12',
     username: SAUCE_USERNAME,
     accessKey: SAUCE_ACCESS_KEY,
     build: CIRCLE_BUILD_NUM
diff --git a/frontend/test/e2e/support/utils.js b/frontend/test/e2e/support/utils.js
index 43e69a6a08da7b135dbfa4601d5eceed597c01a0..89c17fa2f3ab9abd8c5b315d4901ff2e81063b5b 100644
--- a/frontend/test/e2e/support/utils.js
+++ b/frontend/test/e2e/support/utils.js
@@ -7,6 +7,11 @@ import { Driver } from "webchauffeur";
 
 const DEFAULT_TIMEOUT = 50000;
 
+// these are sessions persisted in the fixture dbs, to avoid having to login
+const DEFAULT_SESSIONS = {
+    "bob@metabase.com": "068a6678-db09-4853-b7d5-d0ef6cb9cbc8"
+}
+
 const log = (message) => {
     console.log(message);
 };
@@ -144,11 +149,11 @@ export const checkLoggedIn = async (server, driver, email) => {
 }
 
 const getSessionId = (server, email) => {
-    server.sessions = server.sessions || {};
+    server.sessions = server.sessions || { ...DEFAULT_SESSIONS };
     return server.sessions[email];
 }
 const setSessionId = (server, email, sessionId) => {
-    server.sessions = server.sessions || {};
+    server.sessions = server.sessions || { ...DEFAULT_SESSIONS };
     server.sessions[email] = sessionId;
 }
 
diff --git a/frontend/test/unit/lib/formatting.spec.js b/frontend/test/unit/lib/formatting.spec.js
index f2d01c59fd073588165f9cd86d444632e3448de5..3898e3b737cf2a86570bd87664617e415907022c 100644
--- a/frontend/test/unit/lib/formatting.spec.js
+++ b/frontend/test/unit/lib/formatting.spec.js
@@ -57,6 +57,12 @@ describe('formatting', () => {
             expect(formatValue(37.7749, { column: { base_type: TYPE.Number, special_type: TYPE.Latitude }})).toEqual("37.77490000");
             expect(formatValue(-122.4194, { column: { base_type: TYPE.Number, special_type: TYPE.Longitude }})).toEqual("-122.41940000");
         });
+        it("should return a component for links in jsx mode", () => {
+            expect(isElementOfType(formatValue("http://metabase.com/", { jsx: true }), ExternalLink)).toEqual(true);
+        });
+        it("should return a component for email addresses in jsx mode", () => {
+            expect(isElementOfType(formatValue("tom@metabase.com", { jsx: true }), ExternalLink)).toEqual(true);
+        });
     });
 
     describe("formatUrl", () => {
@@ -68,6 +74,10 @@ describe('formatting', () => {
             expect(isElementOfType(formatUrl("https://metabase.com/", { jsx: true }), ExternalLink)).toEqual(true);
             expect(isElementOfType(formatUrl("mailto:tom@metabase.com", { jsx: true }), ExternalLink)).toEqual(true);
         });
+        it("should not return a link component for unrecognized links in jsx mode", () => {
+            expect(isElementOfType(formatUrl("nonexistent://metabase.com/", { jsx: true }), ExternalLink)).toEqual(false);
+            expect(isElementOfType(formatUrl("metabase.com", { jsx: true }), ExternalLink)).toEqual(false);
+        });
         it("should return a string for javascript:, data:, and other links in jsx mode", () => {
             expect(formatUrl("javascript:alert('pwnd')", { jsx: true })).toEqual("javascript:alert('pwnd')");
             expect(formatUrl("data:text/plain;charset=utf-8,hello%20world", { jsx: true })).toEqual("data:text/plain;charset=utf-8,hello%20world");
diff --git a/frontend/test/unit/visualizations/lib/LineAreaBarRenderer.spec.js b/frontend/test/unit/visualizations/lib/LineAreaBarRenderer.spec.js
index e3e5033981c577cb0fc5b1ce86e4018a9dfac329..1da75bcb676fae7d98d123b3db7b37fcb420e63b 100644
--- a/frontend/test/unit/visualizations/lib/LineAreaBarRenderer.spec.js
+++ b/frontend/test/unit/visualizations/lib/LineAreaBarRenderer.spec.js
@@ -4,7 +4,7 @@ import { formatValue } from "metabase/lib/formatting";
 
 import d3 from "d3";
 
-import { DateTimeColumn, NumberColumn } from "../../support/visualizations";
+import { DateTimeColumn, NumberColumn, StringColumn } from "../../support/visualizations";
 
 let formatTz = (offset) => (offset < 0 ? "-" : "+") + d3.format("02d")(Math.abs(offset)) + ":00"
 
@@ -26,10 +26,12 @@ describe("LineAreaBarRenderer", () => {
 
     it("should display numeric year in X-axis and tooltip correctly", (done) => {
         renderTimeseriesLine({
-            rows: [
-                [2015, 1],
-                [2016, 2],
-                [2017, 3]
+            rowsOfSeries: [
+                [
+                    [2015, 1],
+                    [2016, 2],
+                    [2017, 3]
+                ]
             ],
             unit: "year",
             onHoverChange: (hover) => {
@@ -53,8 +55,9 @@ describe("LineAreaBarRenderer", () => {
                 ["2016-10-03T20:00:00.000" + tz, 1],
                 ["2016-10-03T21:00:00.000" + tz, 1],
             ];
+
             renderTimeseriesLine({
-                rows,
+                rowsOfSeries: [rows],
                 unit: "hour",
                 onHoverChange: (hover) => {
                     let expected = rows.map(row => formatValue(row[0], { column: DateTimeColumn({ unit: "hour" }) }));
@@ -78,7 +81,7 @@ describe("LineAreaBarRenderer", () => {
             ["2016-01-01T04:00:00.000" + tz, 1]
         ];
         renderTimeseriesLine({
-            rows,
+            rowsOfSeries: [rows],
             unit: "hour",
             onHoverChange: (hover) => {
                 expect(formatValue(rows[0][0], { column: DateTimeColumn({ unit: "hour" }) })).toEqual(
@@ -99,6 +102,127 @@ describe("LineAreaBarRenderer", () => {
         dispatchUIEvent(qs("svg .dot"), "mousemove");
     });
 
+    describe("should render correctly a compound line graph", () => {
+        const rowsOfNonemptyCard = [
+            [2015, 1],
+            [2016, 2],
+            [2017, 3]
+        ]
+
+        it("when only second series is not empty", () => {
+            renderTimeseriesLine({
+                rowsOfSeries: [
+                    [], rowsOfNonemptyCard, [], []
+                ],
+                unit: "hour"
+            });
+
+            // A simple check to ensure that lines are rendered as expected
+            expect(qs("svg .line")).not.toBe(null);
+        });
+
+        it("when only first series is not empty", () => {
+            renderTimeseriesLine({
+                rowsOfSeries: [
+                    rowsOfNonemptyCard, [], [], []
+                ],
+                unit: "hour"
+            });
+
+            expect(qs("svg .line")).not.toBe(null);
+        });
+
+        it("when there are many empty and nonempty values ", () => {
+            renderTimeseriesLine({
+                rowsOfSeries: [
+                    [], rowsOfNonemptyCard, [], [], rowsOfNonemptyCard, [], rowsOfNonemptyCard
+                ],
+                unit: "hour"
+            });
+            expect(qs("svg .line")).not.toBe(null);
+        });
+    })
+
+    describe("should render correctly a compound bar graph", () => {
+        it("when only second series is not empty", () => {
+            renderScalarBar({
+                scalars: [
+                    ["Non-empty value", null],
+                    ["Empty value", 25]
+                ]
+            })
+            expect(qs("svg .bar")).not.toBe(null);
+        });
+
+        it("when only first series is not empty", () => {
+            renderScalarBar({
+                scalars: [
+                    ["Non-empty value", 15],
+                    ["Empty value", null]
+                ]
+            })
+            expect(qs("svg .bar")).not.toBe(null);
+        });
+
+        it("when there are many empty and nonempty scalars", () => {
+            renderScalarBar({
+                scalars: [
+                    ["Empty value", null],
+                    ["Non-empty value", 15],
+                    ["2nd empty value", null],
+                    ["2nd non-empty value", 35],
+                    ["3rd empty value", null],
+                    ["4rd empty value", null],
+                    ["3rd non-empty value", 0],
+                ]
+            })
+            expect(qs("svg .bar")).not.toBe(null);
+        });
+    })
+
+    describe('goals', () => {
+        it('should render a goal line', () => {
+            let rows = [
+                ["2016", 1],
+                ["2017", 2],
+            ];
+
+            renderTimeseriesLine({
+                rowsOfSeries: [rows],
+                settings: {
+                    "graph.show_goal": true,
+                    "graph.goal_value": 30
+                }
+            })
+
+            expect(qs('svg .goal .line')).not.toBe(null)
+            expect(qs('svg .goal text')).not.toBe(null)
+            expect(qs('svg .goal text').textContent).toEqual('Goal')
+        })
+
+        it('should render a goal tooltip with the proper value', (done) => {
+            let rows = [
+                ["2016", 1],
+                ["2017", 2],
+            ];
+
+            const goalValue = 30
+            renderTimeseriesLine({
+                rowsOfSeries: [rows],
+                settings: {
+                    "graph.show_goal": true,
+                    "graph.goal_value": goalValue
+                },
+                onHoverChange: (hover) => {
+                    expect(hover.data[0].value).toEqual(goalValue)
+                    done();
+                }
+            })
+            dispatchUIEvent(qs("svg .goal text"), "mouseenter");
+        })
+
+    })
+
     // querySelector shortcut
     const qs = (selector) => element.querySelector(selector);
 
@@ -106,19 +230,41 @@ describe("LineAreaBarRenderer", () => {
     const qsa = (selector) => [...element.querySelectorAll(selector)];
 
     // helper for timeseries line charts
-    const renderTimeseriesLine = ({ rows, onHoverChange, unit }) => {
+    const renderTimeseriesLine = ({ rowsOfSeries, onHoverChange, unit, settings }) => {
         lineAreaBarRenderer(element, {
             chartType: "line",
-            series: [{
+            series: rowsOfSeries.map((rows) => ({
                 data: {
                     "cols" : [DateTimeColumn({ unit }), NumberColumn()],
                     "rows" : rows
                 }
-            }],
+            })),
             settings: {
                 "graph.x_axis.scale": "timeseries",
                 "graph.x_axis.axis_enabled": true,
-                "graph.colors": ["#000000"]
+                "graph.colors": ["#000000"],
+                ...settings,
+            },
+            onHoverChange
+        });
+    }
+
+    const renderScalarBar = ({ scalars, onHoverChange, unit }) => {
+        lineAreaBarRenderer(element, {
+            chartType: "bar",
+            series: scalars.map((scalar) => ({
+                data: {
+                    "cols" : [StringColumn(), NumberColumn()],
+                    "rows" : [scalar]
+                }
+            })),
+            settings: {
+                "bar.scalar_series": true,
+                "funnel.type": "bar",
+                "graph.colors": ["#509ee3", "#9cc177", "#a989c5", "#ef8c8c"],
+                "graph.x_axis.axis_enabled": true,
+                "graph.x_axis.scale": "ordinal",
+                "graph.x_axis._is_numeric": false
             },
             onHoverChange
         });
diff --git a/package.json b/package.json
index 9c49b22ae88877fda9b3559ad0d1959ce7ac8284..4064125a002d224f8246cde8df75391c330c473d 100644
--- a/package.json
+++ b/package.json
@@ -131,7 +131,7 @@
     "promise-loader": "^1.0.0",
     "react-addons-test-utils": "^15.4.2",
     "react-hot-loader": "^1.3.0",
-    "react-test-renderer": "^15.4.2",
+    "react-test-renderer": "^15.5.4",
     "sauce-connect-launcher": "^1.1.1",
     "selenium-webdriver": "^2.53.3",
     "style-loader": "^0.16.1",
@@ -173,15 +173,19 @@
     ]
   },
   "jest": {
+    "moduleNameMapper": {
+      "\\.(css|less)$": "<rootDir>/frontend/test/__mocks__/styleMock.js",
+      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/frontend/test/__mocks__/fileMock.js",
+      "^promise-loader\\?global\\!metabase\\/lib\\/ga-metadata$": "<rootDir>/frontend/src/metabase/lib/ga-metadata.js"
+    },
     "testPathIgnorePatterns": [
       "<rootDir>/frontend/test/"
     ],
     "modulePaths": [
       "<rootDir>/frontend/src"
     ],
-    "moduleNameMapper": {
-      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
-      "\\.(css|less)$": "<rootDir>/frontend/test/__mocks__/styleMock.js"
-    }
+    "setupFiles": [
+      "<rootDir>/frontend/test/metabase-bootstrap.js"
+    ]
   }
 }
diff --git a/resources/frontend_client/app/assets/img/header_rect.svg b/resources/frontend_client/app/assets/img/header_rect.svg
deleted file mode 100644
index 53a652c35f904ea39a5c5a8026b769cf9c17c4f4..0000000000000000000000000000000000000000
--- a/resources/frontend_client/app/assets/img/header_rect.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="58px" height="57px" viewBox="0 0 58 57" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g transform="translate(-393.000000, -53.000000)" fill="#4F9CE0">
-            <g transform="translate(-6.000000, -9.000000)">
-                <g transform="translate(13.000000, -3.000000)">
-                    <rect transform="translate(415.000000, 93.715729) rotate(-45.000000) translate(-415.000000, -93.715729) " x="395" y="73.7157288" width="40" height="40"></rect>
-                </g>
-            </g>
-        </g>
-    </g>
-</svg>
diff --git a/resources/frontend_client/app/img/dashboard_illustration.png b/resources/frontend_client/app/img/dashboard_illustration.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a4cc2c314fa8cb311f64a319a2ae61bc81a2705
Binary files /dev/null and b/resources/frontend_client/app/img/dashboard_illustration.png differ
diff --git a/resources/frontend_client/app/img/dashboard_illustration@2x.png b/resources/frontend_client/app/img/dashboard_illustration@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a4cc2c314fa8cb311f64a319a2ae61bc81a2705
Binary files /dev/null and b/resources/frontend_client/app/img/dashboard_illustration@2x.png differ
diff --git a/resources/frontend_client/app/img/empty_dashboard.png b/resources/frontend_client/app/img/empty_dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..da892b0aebb16435c2284ef13b99d8c36966c45b
Binary files /dev/null and b/resources/frontend_client/app/img/empty_dashboard.png differ
diff --git a/resources/frontend_client/app/img/empty_dashboard@2x.png b/resources/frontend_client/app/img/empty_dashboard@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..da892b0aebb16435c2284ef13b99d8c36966c45b
Binary files /dev/null and b/resources/frontend_client/app/img/empty_dashboard@2x.png differ
diff --git a/resources/kanye-quotes.edn b/resources/kanye-quotes.edn
index 5d60475129bc2e1267ea1b49a58ccc0f3285a9f8..69fef452d8d4ebb496e9b8f93fb239360b7fc142 100644
--- a/resources/kanye-quotes.edn
+++ b/resources/kanye-quotes.edn
@@ -9,6 +9,7 @@
  "Fur pillows are hard to actually sleep on."
  "Here's something that's contrary to popular belief: I actually don't like thinking. I think people think I like to think a lot. And I don't. I do not like to think at all."
  "How many m*****f***ers you done seen with a leather jogging pant?"
+ "I actually don't like thinking. I think people think I like to think a lot. And I don't. I do not like to think at all."
  "I am God's vessel. But my greatest pain in life is that I will never be able to see myself perform live."
  "I am in the lineage of Gil Scott-Heron, great activist-type artists. But I'm also in the linage of a Miles Davis, you know, that liked nice things too."
  "I am not a fan of books. I would never want a book's autograph. I am a proud non-reader of books."
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index 6204cdf0f5bf3ad58e8a88b06e6b2ec8a7e97ba6..6c2af3c37612e335f64821cb334e9c5d7d9cd747 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -3570,14 +3570,79 @@ databaseChangeLog:
   - changeSet:
       id: 54
       author: tlrobinson
+      validCheckSum: '7:695b12a78e897c62c21d41bfb04ab44b'
+      validCheckSum: '7:0857800db71a4757e7202aad4eaed48d'
       changes:
         - addColumn:
             tableName: pulse
-            remarks: 'Skip a scheduled Pulse if none of its questions have any results'
             columns:
               - column:
                   name: skip_if_empty
                   type: boolean
+                  remarks: 'Skip a scheduled Pulse if none of its questions have any results'
                   defaultValueBoolean: false
                   constraints:
                     nullable: false
+  - changeSet:
+      id: 55
+      author: camsaul
+      changes:
+        - addColumn:
+            tableName: report_dashboard
+            columns:
+              - column:
+                  name: archived
+                  type: boolean
+                  remarks: 'Is this Dashboard archived (effectively treated as deleted?)'
+                  defaultValueBoolean: false
+                  constraints:
+                    nullable: false
+              - column:
+                  name: position
+                  type: integer
+                  remarks: 'The position this Dashboard should appear in the Dashboards list, lower-numbered positions appearing before higher numbered ones.'
+        - createTable:
+            tableName: dashboard_favorite
+            remarks: 'Presence of a row here indicates a given User has favorited a given Dashboard.'
+            columns:
+              - column:
+                  name: id
+                  type: int
+                  autoIncrement: true
+                  constraints:
+                    primaryKey: true
+                    nullable: false
+              - column:
+                  name: user_id
+                  type: int
+                  remarks: 'ID of the User who favorited the Dashboard.'
+                  constraints:
+                    nullable: false
+                    references: core_user(id)
+                    foreignKeyName: fk_dashboard_favorite_user_id
+                    deleteCascade: true
+              - column:
+                  name: dashboard_id
+                  type: int
+                  remarks: 'ID of the Dashboard favorited by the User.'
+                  constraints:
+                    nullable: false
+                    references: report_dashboard(id)
+                    foreignKeyName: fk_dashboard_favorite_dashboard_id
+                    deleteCascade: true
+        - addUniqueConstraint:
+            tableName: dashboard_favorite
+            columnNames: user_id, dashboard_id
+            constraintName: unique_dashboard_favorite_user_id_dashboard_id
+        - createIndex:
+            tableName: dashboard_favorite
+            indexName: idx_dashboard_favorite_user_id
+            columns:
+              - column:
+                  name: user_id
+        - createIndex:
+            tableName: dashboard_favorite
+            indexName: idx_dashboard_favorite_dashboard_id
+            columns:
+              - column:
+                  name: dashboard_id
diff --git a/src/metabase/api/activity.clj b/src/metabase/api/activity.clj
index 466f1380ea3d3088da0e61057b9523c609f0c2b2..3cfd7f7db172fc5e1827f69abbc4c6e28498099a 100644
--- a/src/metabase/api/activity.clj
+++ b/src/metabase/api/activity.clj
@@ -1,16 +1,16 @@
 (ns metabase.api.activity
   (:require [clojure.set :as set]
             [compojure.core :refer [GET]]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]]
-                    [models :as models])
-            [metabase.api.common :refer :all]
-            (metabase.models [activity :refer [Activity]]
-                             [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [interface :as mi]
-                             [view-log :refer [ViewLog]])
-            [metabase.models.interface :as mi]))
+            [metabase.api.common :refer [*current-user-id* defendpoint define-routes]]
+            [metabase.models
+             [activity :refer [Activity]]
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [interface :as mi]
+             [view-log :refer [ViewLog]]]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
 (defn- dashcard-activity? [activity]
   (contains? #{:dashboard-add-cards :dashboard-remove-cards}
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index e27c8c855ce6dcc1e2143a40bf4663287d34e84f..b34a27836fa4f7d462cc49cc6568e019a9c2bec8 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -1,37 +1,39 @@
 (ns metabase.api.card
-  (:require [clojure.data :as data]
+  (:require [cheshire.core :as json]
+            [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]])
-            (metabase.api [common :refer :all]
-                          [dataset :as dataset-api]
-                          [label :as label-api])
-            [metabase.events :as events]
-            (metabase.models [card :refer [Card], :as card]
-                             [card-favorite :refer [CardFavorite]]
-                             [card-label :refer [CardLabel]]
-                             [collection :refer [Collection]]
-                             [common :as common]
-                             [database :refer [Database]]
-                             [interface :as mi]
-                             [label :refer [Label]]
-                             [permissions :as perms]
-                             [query :as query]
-                             [table :refer [Table]]
-                             [view-log :refer [ViewLog]])
-            [metabase.public-settings :as public-settings]
-            [metabase.query-processor :as qp]
+            [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase
+             [events :as events]
+             [public-settings :as public-settings]
+             [query-processor :as qp]
+             [util :as u]]
+            [metabase.api
+             [common :as api]
+             [dataset :as dataset-api]
+             [label :as label-api]]
+            [metabase.models
+             [card :as card :refer [Card]]
+             [card-favorite :refer [CardFavorite]]
+             [card-label :refer [CardLabel]]
+             [collection :refer [Collection]]
+             [database :refer [Database]]
+             [interface :as mi]
+             [label :refer [Label]]
+             [permissions :as perms]
+             [query :as query]
+             [table :refer [Table]]
+             [view-log :refer [ViewLog]]]
             [metabase.query-processor.middleware.cache :as cache]
             [metabase.query-processor.util :as qputil]
-            [metabase.util :as u]
-            [metabase.util.schema :as su])
+            [metabase.util.schema :as su]
+            [ring.util.codec :as codec]
+            [schema.core :as s]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]])
   (:import java.util.UUID))
 
-
 ;;; ------------------------------------------------------------ Hydration ------------------------------------------------------------
 
 (defn- ^:deprecated hydrate-labels
@@ -49,7 +51,7 @@
   "Efficiently add `favorite` status for a large collection of `Cards`."
   [cards]
   (when (seq cards)
-    (let [favorite-card-ids (set (db/select-field :card_id CardFavorite, :owner_id *current-user-id*, :card_id [:in (map :id cards)]))]
+    (let [favorite-card-ids (db/select-field :card_id CardFavorite, :owner_id api/*current-user-id*, :card_id [:in (map :id cards)])]
       (for [card cards]
         (assoc card :favorite (contains? favorite-card-ids (:id card)))))))
 
@@ -64,12 +66,12 @@
 (defn- cards:mine
   "Return all `Cards` created by current user."
   []
-  (db/select Card, :creator_id *current-user-id*, :archived false, {:order-by [[:%lower.name :asc]]}))
+  (db/select Card, :creator_id api/*current-user-id*, :archived false, {:order-by [[:%lower.name :asc]]}))
 
 (defn- cards:fav
   "Return all `Cards` favorited by the current user."
   []
-  (->> (hydrate (db/select [CardFavorite :card_id], :owner_id *current-user-id*)
+  (->> (hydrate (db/select [CardFavorite :card_id], :owner_id api/*current-user-id*)
                 :card)
        (map :card)
        (filter (complement :archived))
@@ -98,7 +100,7 @@
   []
   (cards-with-ids (map :model_id (db/select [ViewLog :model_id [:%max.timestamp :max]]
                                    :model   "card"
-                                   :user_id *current-user-id*
+                                   :user_id api/*current-user-id*
                                    {:group-by [:model_id]
                                     :order-by [[:max :desc]]
                                     :limit    10}))))
@@ -139,13 +141,12 @@
     ;; 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)]]}))))
+    (api/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-slug]
-  (println "collection-slug:" collection-slug) ; NOCOMMIT
   (let [cards (-> ((filter-option->fn (or filter-option :all)) model-id)
                   (hydrate :creator :collection)
                   hydrate-labels
@@ -166,7 +167,7 @@
   "Schema for a valid card filter option."
   (apply s/enum (map name (keys filter-option->fn))))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Get all the `Cards`. Option filter param `f` can be used to change the set of Cards that are returned; default is `all`,
    but other options include `mine`, `fav`, `database`, `table`, `recent`, `popular`, and `archived`. See corresponding implementation
    functions above for the specific behavior of each filter option. :card_index:
@@ -182,15 +183,15 @@
   {f (s/maybe CardFilterOption), model_id (s/maybe su/IntGreaterThanZero), label (s/maybe su/NonBlankString), collection (s/maybe s/Str)}
   (let [f (keyword f)]
     (when (contains? #{:database :table} f)
-      (checkp (integer? model_id) "id" (format "id is required parameter when filter mode is '%s'" (name f)))
+      (api/checkp (integer? model_id) "id" (format "id is required parameter when filter mode is '%s'" (name f)))
       (case f
-        :database (read-check Database model_id)
-        :table    (read-check Database (db/select-one-field :db_id Table, :id model_id))))
+        :database (api/read-check Database model_id)
+        :table    (api/read-check Database (db/select-one-field :db_id Table, :id model_id))))
     (->> (cards-for-filter-option f model_id label collection)
          (filterv mi/can-read?)))) ; filterv because we want make sure all the filtering is done while current user perms set is still bound
 
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Create a new `Card`."
   [:as {{:keys [dataset_query description display name visualization_settings collection_id]} :body}]
   {name                   su/NonBlankString
@@ -199,13 +200,13 @@
    visualization_settings su/Map
    collection_id          (s/maybe su/IntGreaterThanZero)}
   ;; check that we have permissions to run the query that we're trying to save
-  (check-403 (perms/set-has-full-permissions-for-set? @*current-user-permissions-set* (card/query-perms-set dataset_query :write)))
+  (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* (card/query-perms-set dataset_query :write)))
   ;; check that we have permissions for the collection we're trying to save this card to, if applicable
   (when collection_id
-    (check-403 (perms/set-has-full-permissions? @*current-user-permissions-set* (perms/collection-readwrite-path collection_id))))
+    (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set* (perms/collection-readwrite-path collection_id))))
   ;; everything is g2g, now save the card
   (->> (db/insert! Card
-         :creator_id             *current-user-id*
+         :creator_id             api/*current-user-id*
          :dataset_query          dataset_query
          :description            description
          :display                display
@@ -215,28 +216,28 @@
        (events/publish-event! :card-create)))
 
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Get `Card` with ID."
   [id]
-  (-> (read-check Card id)
+  (-> (api/read-check Card id)
       (hydrate :creator :dashboard_count :labels :can_write :collection)
-      (assoc :actor_id *current-user-id*)
+      (assoc :actor_id api/*current-user-id*)
       (->> (events/publish-event! :card-read))
       (dissoc :actor_id)))
 
 (defn- check-permissions-for-collection
   "Check that we have permissions to add or remove cards from `Collection` with COLLECTION-ID."
   [collection-id]
-  (check-403 (perms/set-has-full-permissions? @*current-user-permissions-set* (perms/collection-readwrite-path collection-id))))
+  (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set* (perms/collection-readwrite-path collection-id))))
 
 (defn check-data-permissions-for-query
   "Check that we have *data* permissions to run the QUERY in question."
   [query]
   {:pre [(map? query)]}
-  (check-403 (perms/set-has-full-permissions-for-set? @*current-user-permissions-set* (card/query-perms-set query :read))))
+  (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* (card/query-perms-set query :read))))
 
 ;; TODO - This endpoint desperately needs to be broken out into smaller, bite-sized chunks
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `Card`."
   [id :as {{:keys [dataset_query description display name visualization_settings archived collection_id enable_embedding embedding_params], :as body} :body}]
   {name                   (s/maybe su/NonBlankString)
@@ -248,7 +249,7 @@
    enable_embedding       (s/maybe s/Bool)
    embedding_params       (s/maybe su/EmbeddingParams)
    collection_id          (s/maybe su/IntGreaterThanZero)}
-  (let [card (write-check Card id)]
+  (let [card (api/write-check Card id)]
     ;; if we're changing the `collection_id` of the Card, make sure we have write permissions for the new group
     (when (and (not (nil? collection_id)) (not= (:collection_id card) collection_id))
       (check-permissions-for-collection collection_id))
@@ -265,21 +266,14 @@
                    (not= enable_embedding (:enable_embedding card)))
               (and embedding_params
                    (not= embedding_params (:embedding_params card))))
-      (check-embedding-enabled)
-      (check-superuser))
+      (api/check-embedding-enabled)
+      (api/check-superuser))
     ;; ok, now save the Card
     (db/update! Card id
-      (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}))))
+      ;; `collection_id` and `description` can be `nil` (in order to unset them). Other values should only be modified if they're passed in as non-nil
+      (u/select-keys-when body
+        :present #{:collection_id :description}
+        :non-nil #{:dataset_query :display :name :visualization_settings :archived :enable_embedding :embedding_params}))
     (let [event (cond
                   ;; card was archived
                   (and archived
@@ -288,49 +282,52 @@
                   (and (false? archived)
                        (:archived card))       :card-unarchive
                   :else                        :card-update)
-          card   (assoc (Card id) :actor_id *current-user-id*)]
+          card   (assoc (Card id) :actor_id api/*current-user-id*)]
       (events/publish-event! event card)
       ;; include same information returned by GET /api/card/:id since frontend replaces the Card it currently has with returned one -- See #4142
       (hydrate card :creator :dashboard_count :labels :can_write :collection))))
 
 
-(defendpoint DELETE "/:id"
+;; TODO - Pretty sure this endpoint is not actually used any more, since Cards are supposed to get archived (via PUT /api/card/:id) instead of deleted.
+;;        Should we remove this?
+(api/defendpoint DELETE "/:id"
   "Delete a `Card`."
   [id]
-  (let [card (write-check Card id)]
+  (log/warn "DELETE /api/card/:id is deprecated. Instead of deleting a Card, you should change its `archived` value via PUT /api/card/:id.")
+  (let [card (api/write-check Card id)]
     (db/delete! Card :id id)
-    (events/publish-event! :card-delete (assoc card :actor_id *current-user-id*)))
-  generic-204-no-content)
+    (events/publish-event! :card-delete (assoc card :actor_id api/*current-user-id*)))
+  api/generic-204-no-content)
 
 
 ;;; ------------------------------------------------------------ Favoriting ------------------------------------------------------------
 
-(defendpoint POST "/:card-id/favorite"
+(api/defendpoint POST "/:card-id/favorite"
   "Favorite a Card."
   [card-id]
-  (read-check Card card-id)
-  (db/insert! CardFavorite :card_id card-id, :owner_id *current-user-id*))
+  (api/read-check Card card-id)
+  (db/insert! CardFavorite :card_id card-id, :owner_id api/*current-user-id*))
 
 
-(defendpoint DELETE "/:card-id/favorite"
+(api/defendpoint DELETE "/:card-id/favorite"
   "Unfavorite a Card."
   [card-id]
-  (read-check Card card-id)
-  (let-404 [id (db/select-one-id CardFavorite :card_id card-id, :owner_id *current-user-id*)]
+  (api/read-check Card card-id)
+  (api/let-404 [id (db/select-one-id CardFavorite :card_id card-id, :owner_id api/*current-user-id*)]
     (db/delete! CardFavorite, :id id))
-  generic-204-no-content)
+  api/generic-204-no-content)
 
 
 ;;; ------------------------------------------------------------ Editing Card Labels ------------------------------------------------------------
 
 
-(defendpoint POST "/:card-id/labels"
+(api/defendpoint POST "/:card-id/labels"
   "Update the set of `Labels` that apply to a `Card`.
    (This endpoint is considered DEPRECATED as Labels will be removed in a future version of Metabase.)"
   [card-id :as {{:keys [label_ids]} :body}]
   {label_ids [su/IntGreaterThanZero]}
   (label-api/warn-about-labels-being-deprecated)
-  (write-check Card card-id)
+  (api/write-check Card card-id)
   (let [[labels-to-remove labels-to-add] (data/diff (set (db/select-field :label_id CardLabel :card_id card-id))
                                                     (set label_ids))]
     (when (seq labels-to-remove)
@@ -345,7 +342,7 @@
 (defn- move-cards-to-collection! [new-collection-id-or-nil card-ids]
   ;; if moving to a collection, make sure we have write perms for it
   (when new-collection-id-or-nil
-    (write-check Collection new-collection-id-or-nil))
+    (api/write-check Collection new-collection-id-or-nil))
   ;; for each affected card...
   (when (seq card-ids)
     (let [cards (db/select [Card :id :collection_id :dataset_query]
@@ -355,15 +352,15 @@
                                        [:= :collection_id nil])]]})] ; poisioned NULLs = ick
       ;; ...check that we have write permissions for it...
       (doseq [card cards]
-        (write-check card))
+        (api/write-check card))
       ;; ...and check that we have write permissions for the old collections if applicable
       (doseq [old-collection-id (set (filter identity (map :collection_id cards)))]
-        (write-check Collection old-collection-id)))
+        (api/write-check Collection old-collection-id)))
     ;; ok, everything checks out. Set the new `collection_id` for all the Cards
     (db/update-where! Card {:id [:in (set card-ids)]}
       :collection_id new-collection-id-or-nil)))
 
-(defendpoint POST "/collections"
+(api/defendpoint POST "/collections"
   "Bulk update endpoint for Card Collections. Move a set of `Cards` with CARD_IDS into a `Collection` with COLLECTION_ID,
    or remove them from any Collections by passing a `null` COLLECTION_ID."
   [:as {{:keys [card_ids collection_id]} :body}]
@@ -401,23 +398,23 @@
               :or   {constraints dataset-api/default-query-constraints
                      context     :question}}]
   {:pre [(u/maybe? sequential? parameters)]}
-  (let [card    (read-check Card card-id)
+  (let [card    (api/read-check Card card-id)
         query   (query-for-card card parameters constraints)
-        options {:executed-by  *current-user-id*
+        options {:executed-by  api/*current-user-id*
                  :context      context
                  :card-id      card-id
                  :dashboard-id dashboard-id}]
-    (check-not-archived card)
+    (api/check-not-archived card)
     (qp/dataset-query query options)))
 
-(defendpoint POST "/:card-id/query"
+(api/defendpoint POST "/:card-id/query"
   "Run the query associated with a Card."
   [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"
+(api/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)}
@@ -427,7 +424,7 @@
                           :constraints nil
                           :context     :csv-download))))
 
-(defendpoint POST "/:card-id/query/json"
+(api/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)}
@@ -440,44 +437,44 @@
 
 ;;; ------------------------------------------------------------ Sharing is Caring ------------------------------------------------------------
 
-(defendpoint POST "/:card-id/public_link"
+(api/defendpoint POST "/:card-id/public_link"
   "Generate publically-accessible links for this Card. Returns UUID to be used in public links.
    (If this Card has already been shared, it will return the existing public link rather than creating a new one.)
    Public sharing must be enabled."
   [card-id]
-  (check-superuser)
-  (check-public-sharing-enabled)
-  (check-not-archived (read-check Card card-id))
+  (api/check-superuser)
+  (api/check-public-sharing-enabled)
+  (api/check-not-archived (api/read-check Card card-id))
   {:uuid (or (db/select-one-field :public_uuid Card :id card-id)
              (u/prog1 (str (UUID/randomUUID))
                (db/update! Card card-id
                  :public_uuid       <>
-                 :made_public_by_id *current-user-id*)))})
+                 :made_public_by_id api/*current-user-id*)))})
 
-(defendpoint DELETE "/:card-id/public_link"
+(api/defendpoint DELETE "/:card-id/public_link"
   "Delete the publically-accessible link to this Card."
   [card-id]
-  (check-superuser)
-  (check-public-sharing-enabled)
-  (check-exists? Card :id card-id, :public_uuid [:not= nil])
+  (api/check-superuser)
+  (api/check-public-sharing-enabled)
+  (api/check-exists? Card :id card-id, :public_uuid [:not= nil])
   (db/update! Card card-id
     :public_uuid       nil
     :made_public_by_id nil)
   {:status 204, :body nil})
 
-(defendpoint GET "/public"
+(api/defendpoint GET "/public"
   "Fetch a list of Cards with public UUIDs. These cards are publically-accessible *if* public sharing is enabled."
   []
-  (check-superuser)
-  (check-public-sharing-enabled)
+  (api/check-superuser)
+  (api/check-public-sharing-enabled)
   (db/select [Card :name :id :public_uuid], :public_uuid [:not= nil], :archived false))
 
-(defendpoint GET "/embeddable"
+(api/defendpoint GET "/embeddable"
   "Fetch a list of Cards where `enable_embedding` is `true`. The cards can be embedded using the embedding endpoints and a signed JWT."
   []
-  (check-superuser)
-  (check-embedding-enabled)
+  (api/check-superuser)
+  (api/check-embedding-enabled)
   (db/select [Card :name :id], :enable_embedding true, :archived false))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj
index 16e2a1c05cad4b72760bdc54a660b595c9ef51c9..a148fffd7c1912d81782d49a07ad84acfac6a55e 100644
--- a/src/metabase/api/collection.clj
+++ b/src/metabase/api/collection.clj
@@ -1,15 +1,16 @@
 (ns metabase.api.collection
   "/api/collection endpoints."
-  (:require [compojure.core :refer [GET POST DELETE PUT]]
-            [schema.core :as s]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
+  (:require [compojure.core :refer [GET POST PUT]]
             [metabase.api.common :as api]
-            (metabase.models [card :refer [Card]]
-                             [collection :refer [Collection], :as collection]
-                             [interface :as mi])
-            [metabase.util.schema :as su]))
-
+            [metabase.models
+             [card :refer [Card]]
+             [collection :as collection :refer [Collection]]
+             [interface :as mi]]
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
 (api/defendpoint GET "/"
   "Fetch a list of all Collections that the current user has read permissions for.
diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index f1e2b8f9ddf67363b2e921adff0e18a0d7a3a5a5..0e18189aa8075b6306801d0ece1df95d38337e2a 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -1,16 +1,15 @@
 (ns metabase.api.common
   "Dynamic variables and utility functions/macros for writing API functions."
-  (:require [clojure.tools.logging :as log]
-            (clojure [string :as s]
-                     [walk :as walk])
-            [cheshire.core :as json]
+  (:require [clojure.string :as s]
+            [clojure.tools.logging :as log]
             [compojure.core :refer [defroutes]]
             [medley.core :as m]
-            [toucan.db :as db]
+            [metabase
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.api.common.internal :refer :all]
             [metabase.models.interface :as mi]
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u]))
+            [toucan.db :as db]))
 
 (declare check-403 check-404)
 
@@ -317,4 +316,5 @@
   "Check that the OBJECT is not `:archived`, or throw a `404`. Returns OBJECT as-is if check passes."
   [object]
   (u/prog1 object
-    (check-404 (not (:archived object)))))
+    (check (not (:archived object))
+      [404 "The object has been archived."])))
diff --git a/src/metabase/api/common/internal.clj b/src/metabase/api/common/internal.clj
index 08f4792d8257571eecd84c88f9b81e6de791562c..f4f91dff7f5606c91d8b84e45ec114e50daabb6a 100644
--- a/src/metabase/api/common/internal.clj
+++ b/src/metabase/api/common/internal.clj
@@ -5,10 +5,9 @@
             [clojure.string :as str]
             [clojure.tools.logging :as log]
             [medley.core :as m]
-            [schema.core :as s]
-            metabase.logger
             [metabase.util :as u]
-            [metabase.util.schema :as su])
+            [metabase.util.schema :as su]
+            [schema.core :as s])
   (:import java.sql.SQLException))
 
 ;;; +------------------------------------------------------------------------------------------------------------------------+
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index a80203a4e15c1ffff93bf10151ab03cbf7588ffc..59aae149965f2bd58d1eb4e34c5e35bda5a9760b 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -1,45 +1,63 @@
 (ns metabase.api.dashboard
   "/api/dashboard endpoints."
-  (:require [compojure.core :refer [GET POST PUT DELETE]]
+  (:require [clojure.tools.logging :as log]
+            [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase
+             [events :as events]
+             [util :as u]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard :as dashboard :refer [Dashboard]]
+             [dashboard-card :refer [create-dashboard-card! DashboardCard delete-dashboard-card! update-dashboard-card!]]
+             [dashboard-favorite :refer [DashboardFavorite]]
+             [interface :as mi]
+             [revision :as revision]]
+            [metabase.util.schema :as su]
             [schema.core :as s]
-            [metabase.events :as events]
-            [metabase.api.common :refer :all]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            (metabase.models [card :refer [Card]]
-                             [common :as common]
-                             [dashboard :refer [Dashboard], :as dashboard]
-                             [dashboard-card :refer [DashboardCard create-dashboard-card! update-dashboard-card! delete-dashboard-card!]]
-                             [interface :as mi]
-                             [revision :as revision])
-            [metabase.util :as u]
-            [metabase.util.schema :as su])
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]])
   (:import java.util.UUID))
 
+(defn- hydrate-favorites
+  "Efficiently hydrate the `:favorite` status (whether the current User has favorited it) for a group of Dashboards."
+  [dashboards]
+  (let [favorite-dashboard-ids (when (seq dashboards)
+                                 (db/select-field :dashboard_id DashboardFavorite
+                                   :user_id      api/*current-user-id*
+                                   :dashboard_id [:in (set (map u/get-id dashboards))]))]
+    (for [dashboard dashboards]
+      (assoc dashboard
+        :favorite (contains? favorite-dashboard-ids (u/get-id dashboard))))))
 
 (defn- dashboards-list [filter-option]
-  (filter mi/can-read? (-> (db/select Dashboard {:where    (case (or (keyword filter-option) :all)
-                                                                 :all  true
-                                                                 :mine [:= :creator_id *current-user-id*])
-                                                     :order-by [:%lower.name]})
-                               (hydrate :creator))))
-
-(defendpoint GET "/"
+  (as-> (db/select Dashboard {:where    [:and (case (or (keyword filter-option) :all)
+                                                :all  true
+                                                :mine [:= :creator_id api/*current-user-id*])
+                                              [:= :archived (= (keyword filter-option) :archived)]]
+                              :order-by [:%lower.name]}) <>
+    (hydrate <> :creator)
+    (filter mi/can-read? <>)
+    (hydrate-favorites <>)))
+
+(api/defendpoint GET "/"
   "Get `Dashboards`. With filter option `f` (default `all`), restrict results as follows:
 
-  *  `all` - Return all `Dashboards`.
-  *  `mine` - Return `Dashboards` created by the current user."
+  *  `all`      - Return all Dashboards.
+  *  `mine`     - Return Dashboards created by the current user.
+  *  `archived` - Return Dashboards that have been archived. (By default, these are *excluded*.)"
   [f]
-  {f (s/maybe (s/enum "all" "mine"))}
+  {f (s/maybe (s/enum "all" "mine" "archived"))}
   (dashboards-list f))
 
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Create a new `Dashboard`."
   [:as {{:keys [name parameters], :as dashboard} :body}]
   {name       su/NonBlankString
    parameters [su/Map]}
-  (dashboard/create-dashboard! dashboard *current-user-id*))
+  (dashboard/create-dashboard! dashboard api/*current-user-id*))
 
 (defn- hide-unreadable-card
   "If CARD is unreadable, replace it with an object containing only its `:id`."
@@ -56,22 +74,23 @@
                                                        (update :card hide-unreadable-card)
                                                        (update :series (partial mapv hide-unreadable-card)))))))
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Get `Dashboard` with ID."
   [id]
   (u/prog1 (-> (Dashboard id)
                (hydrate :creator [:ordered_cards [:card :creator] :series])
-               read-check
+               api/read-check
+               api/check-not-archived
                hide-unreadable-cards)
-    (events/publish-event! :dashboard-read (assoc <> :actor_id *current-user-id*))))
+    (events/publish-event! :dashboard-read (assoc <> :actor_id api/*current-user-id*))))
 
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `Dashboard`.
 
    Usually, you just need write permissions for this Dashboard to do this (which means you have appropriate permissions for the Cards belonging to this Dashboard),
    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}]
+  [id :as {{:keys [description name parameters caveats points_of_interest show_in_getting_started enable_embedding embedding_params position archived], :as dashboard} :body}]
   {name                    (s/maybe su/NonBlankString)
    description             (s/maybe s/Str)
    caveats                 (s/maybe s/Str)
@@ -79,137 +98,167 @@
    show_in_getting_started (s/maybe s/Bool)
    enable_embedding        (s/maybe s/Bool)
    embedding_params        (s/maybe su/EmbeddingParams)
-   parameters              (s/maybe [su/Map])}
-  (let [dash (write-check Dashboard id)]
+   parameters              (s/maybe [su/Map])
+   position                (s/maybe su/IntGreaterThanZero)
+   archived                (s/maybe s/Bool)}
+  (let [dash (api/write-check Dashboard id)]
     ;; you must be a superuser to change the value of `enable_embedding` or `embedding_params`. Embedding must be enabled
     (when (or (and (not (nil? enable_embedding))
                    (not= enable_embedding (:enable_embedding dash)))
               (and embedding_params
                    (not= embedding_params (:embedding_params dash))))
-      (check-embedding-enabled)
-      (check-superuser)))
-  (check-500 (-> (assoc dashboard :id id)
-                 (dashboard/update-dashboard! *current-user-id*))))
-
-
-(defendpoint DELETE "/:id"
+      (api/check-embedding-enabled)
+      (api/check-superuser)))
+  (api/check-500
+   (db/update! Dashboard id
+     ;; description, position are allowed to be `nil`. Everything else must be non-nil
+     (u/select-keys-when dashboard
+       :present #{:description :position}
+       :non-nil #{:name :parameters :caveats :points_of_interest :show_in_getting_started :enable_embedding :embedding_params :archived})))
+  ;; now publish an event and return the updated Dashboard
+  (u/prog1 (Dashboard id)
+    (events/publish-event! :dashboard-update (assoc <> :actor_id api/*current-user-id*))))
+
+
+;; TODO - We can probably remove this in the near future since it should no longer be needed now that we're going to be setting `:archived` to `true` via the `PUT` endpoint instead
+(api/defendpoint DELETE "/:id"
   "Delete a `Dashboard`."
   [id]
-  (let [dashboard (write-check Dashboard id)]
+  (log/warn "DELETE /api/dashboard/:id is deprecated. Instead of deleting a Dashboard, you should change its `archived` value via PUT /api/dashboard/:id.")
+  (let [dashboard (api/write-check Dashboard id)]
     (db/delete! Dashboard :id id)
-    (events/publish-event! :dashboard-delete (assoc dashboard :actor_id *current-user-id*)))
-  generic-204-no-content)
+    (events/publish-event! :dashboard-delete (assoc dashboard :actor_id api/*current-user-id*)))
+  api/generic-204-no-content)
 
 
-(defendpoint POST "/:id/cards"
+;; TODO - param should be `card_id`, not `cardId` (fix here + on frontend at the same time)
+(api/defendpoint POST "/:id/cards"
   "Add a `Card` to a `Dashboard`."
   [id :as {{:keys [cardId parameter_mappings series] :as dashboard-card} :body}]
   {cardId             su/IntGreaterThanZero
    parameter_mappings [su/Map]}
-  (write-check Dashboard id)
-  (read-check Card cardId)
+  (api/check-not-archived (api/write-check Dashboard id))
+  (api/check-not-archived (api/read-check Card cardId))
   (let [defaults       {:dashboard_id           id
                         :card_id                cardId
                         :visualization_settings {}
-                        :creator_id             *current-user-id*
+                        :creator_id             api/*current-user-id*
                         :series                 (or series [])}
         dashboard-card (-> (merge dashboard-card defaults)
                            (update :series #(filter identity (map :id %))))]
-    (u/prog1 (check-500 (create-dashboard-card! dashboard-card))
-      (events/publish-event! :dashboard-add-cards {:id id, :actor_id *current-user-id*, :dashcards [<>]}))))
+    (u/prog1 (api/check-500 (create-dashboard-card! dashboard-card))
+      (events/publish-event! :dashboard-add-cards {:id id, :actor_id api/*current-user-id*, :dashcards [<>]}))))
 
 
 ;; TODO - we should use schema to validate the format of the Cards :D
-(defendpoint PUT "/:id/cards"
+(api/defendpoint PUT "/:id/cards"
   "Update `Cards` on a `Dashboard`. Request body should have the form:
 
-    {:cards [{:id ...
-              :sizeX ...
-              :sizeY ...
-              :row ...
-              :col ...
+    {:cards [{:id     ...
+              :sizeX  ...
+              :sizeY  ...
+              :row    ...
+              :col    ...
               :series [{:id 123
                         ...}]} ...]}"
   [id :as {{:keys [cards]} :body}]
-  (write-check Dashboard id)
+  (api/check-not-archived (api/write-check Dashboard id))
   (let [dashcard-ids (db/select-ids DashboardCard, :dashboard_id id)]
     (doseq [{dashcard-id :id, :as dashboard-card} cards]
       ;; ensure the dashcard we are updating is part of the given dashboard
       (when (contains? dashcard-ids dashcard-id)
         (update-dashboard-card! (update dashboard-card :series #(filter identity (map :id %)))))))
-  (events/publish-event! :dashboard-reposition-cards {:id id, :actor_id *current-user-id*, :dashcards cards})
+  (events/publish-event! :dashboard-reposition-cards {:id id, :actor_id api/*current-user-id*, :dashcards cards})
   {:status :ok})
 
 
-(defendpoint DELETE "/:id/cards"
+(api/defendpoint DELETE "/:id/cards"
   "Remove a `DashboardCard` from a `Dashboard`."
   [id dashcardId]
   {dashcardId su/IntStringGreaterThanZero}
-  (write-check Dashboard id)
+  (api/check-not-archived (api/write-check Dashboard id))
   (when-let [dashboard-card (DashboardCard (Integer/parseInt dashcardId))]
-    (check-500 (delete-dashboard-card! dashboard-card *current-user-id*))
+    (api/check-500 (delete-dashboard-card! dashboard-card api/*current-user-id*))
     {:success true})) ; TODO - why doesn't this return a 204 'No Content' response?
 
 
-(defendpoint GET "/:id/revisions"
+(api/defendpoint GET "/:id/revisions"
   "Fetch `Revisions` for `Dashboard` with ID."
   [id]
-  (read-check Dashboard id)
+  (api/read-check Dashboard id)
   (revision/revisions+details Dashboard id))
 
 
-(defendpoint POST "/:id/revert"
+(api/defendpoint POST "/:id/revert"
   "Revert a `Dashboard` to a prior `Revision`."
   [id :as {{:keys [revision_id]} :body}]
   {revision_id su/IntGreaterThanZero}
-  (write-check Dashboard id)
+  (api/write-check Dashboard id)
   (revision/revert!
     :entity      Dashboard
     :id          id
-    :user-id     *current-user-id*
+    :user-id     api/*current-user-id*
     :revision-id revision_id))
 
 
+;;; ------------------------------------------------------------ Favoriting ------------------------------------------------------------
+
+(api/defendpoint POST "/:id/favorite"
+  "Favorite a Dashboard."
+  [id]
+  (api/check-not-archived (api/read-check Dashboard id))
+  (db/insert! DashboardFavorite :dashboard_id id, :user_id api/*current-user-id*))
+
+
+(api/defendpoint DELETE "/:id/favorite"
+  "Unfavorite a Dashboard."
+  [id]
+  (api/check-not-archived (api/read-check Dashboard id))
+  (api/let-404 [favorite-id (db/select-one-id DashboardFavorite :dashboard_id id, :user_id api/*current-user-id*)]
+    (db/delete! DashboardFavorite, :id favorite-id))
+  api/generic-204-no-content)
+
+
 ;;; ------------------------------------------------------------ Sharing is Caring ------------------------------------------------------------
 
-(defendpoint POST "/:dashboard-id/public_link"
+(api/defendpoint POST "/:dashboard-id/public_link"
   "Generate publically-accessible links for this Dashboard. Returns UUID to be used in public links.
    (If this Dashboard has already been shared, it will return the existing public link rather than creating a new one.)
    Public sharing must be enabled."
   [dashboard-id]
-  (check-superuser)
-  (check-public-sharing-enabled)
-  (read-check Dashboard dashboard-id)
+  (api/check-superuser)
+  (api/check-public-sharing-enabled)
+  (api/check-not-archived (api/read-check Dashboard dashboard-id))
   {:uuid (or (db/select-one-field :public_uuid Dashboard :id dashboard-id)
              (u/prog1 (str (UUID/randomUUID))
                (db/update! Dashboard dashboard-id
                  :public_uuid       <>
-                 :made_public_by_id *current-user-id*)))})
+                 :made_public_by_id api/*current-user-id*)))})
 
-(defendpoint DELETE "/:dashboard-id/public_link"
+(api/defendpoint DELETE "/:dashboard-id/public_link"
   "Delete the publically-accessible link to this Dashboard."
   [dashboard-id]
-  (check-superuser)
-  (check-public-sharing-enabled)
-  (check-exists? Dashboard :id dashboard-id, :public_uuid [:not= nil])
+  (api/check-superuser)
+  (api/check-public-sharing-enabled)
+  (api/check-exists? Dashboard :id dashboard-id, :public_uuid [:not= nil], :archived false)
   (db/update! Dashboard dashboard-id
     :public_uuid       nil
     :made_public_by_id nil)
   {:status 204, :body nil})
 
-(defendpoint GET "/public"
+(api/defendpoint GET "/public"
   "Fetch a list of Dashboards with public UUIDs. These dashboards are publically-accessible *if* public sharing is enabled."
   []
-  (check-superuser)
-  (check-public-sharing-enabled)
-  (db/select [Dashboard :name :id :public_uuid], :public_uuid [:not= nil]))
+  (api/check-superuser)
+  (api/check-public-sharing-enabled)
+  (db/select [Dashboard :name :id :public_uuid], :public_uuid [:not= nil], :archived false))
 
-(defendpoint GET "/embeddable"
+(api/defendpoint GET "/embeddable"
   "Fetch a list of Dashboards where `enable_embedding` is `true`. The dashboards can be embedded using the embedding endpoints and a signed JWT."
   []
-  (check-superuser)
-  (check-embedding-enabled)
-  (db/select [Dashboard :name :id], :enable_embedding true))
+  (api/check-superuser)
+  (api/check-embedding-enabled)
+  (db/select [Dashboard :name :id], :enable_embedding true, :archived false))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj
index c83433a7dfe6f79ab00de214f528e7c6c9fba0d1..2b64bc3dc6f206540d7acf1102dc420514a2dba0 100644
--- a/src/metabase/api/database.clj
+++ b/src/metabase/api/database.clj
@@ -2,23 +2,25 @@
   "/api/database endpoints."
   (:require [clojure.string :as str]
             [clojure.tools.logging :as log]
-            [compojure.core :refer [GET POST PUT DELETE]]
+            [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [events :as events]
+             [sample-data :as sample-data]
+             [util :as u]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [database :as database :refer [Database protected-password]]
+             [field :refer [Field]]
+             [interface :as mi]
+             [permissions :as perms]
+             [table :refer [Table]]]
+            [metabase.util.schema :as su]
             [schema.core :as s]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            [metabase.api.common :refer :all]
-            (metabase [config :as config]
-                      [driver :as driver]
-                      [events :as events])
-            (metabase.models common
-                             [database :refer [Database protected-password], :as database]
-                             [field :refer [Field]]
-                             [interface :as mi]
-                             [permissions :as perms]
-                             [table :refer [Table]])
-            (metabase [sample-data :as sample-data]
-                      [util :as u])
-            [metabase.util.schema :as su]))
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
 (def DBEngine
   "Schema for a valid database engine name, e.g. `h2` or `postgres`."
@@ -39,7 +41,7 @@
 
 (defn- add-native-perms-info [dbs]
   (for [db dbs]
-    (let [user-has-perms? (fn [path-fn] (perms/set-has-full-permissions? @*current-user-permissions-set* (path-fn (u/get-id db))))]
+    (let [user-has-perms? (fn [path-fn] (perms/set-has-full-permissions? @api/*current-user-permissions-set* (path-fn (u/get-id db))))]
       (assoc db :native_permissions (cond
                                       (user-has-perms? perms/native-readwrite-path) :write
                                       (user-has-perms? perms/native-read-path)      :read
@@ -51,7 +53,7 @@
                              dbs
                              (add-tables dbs)))))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Fetch all `Databases`."
   [include_tables]
   (or (dbs-list include_tables)
@@ -60,16 +62,16 @@
 
 ;;; ------------------------------------------------------------ GET /api/database/:id ------------------------------------------------------------
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Get `Database` with ID."
   [id]
-  (read-check Database id))
+  (api/read-check Database id))
 
 
 ;;; ------------------------------------------------------------ GET /api/database/:id/metadata ------------------------------------------------------------
 
 (defn- db-metadata [id]
-  (-> (read-check Database id)
+  (-> (api/read-check Database id)
       (hydrate [:tables [:fields :target :values] :segments :metrics])
       (update :tables   (fn [tables]
                           (for [table tables
@@ -78,7 +80,7 @@
                                 (update :segments (partial filter mi/can-read?))
                                 (update :metrics  (partial filter mi/can-read?))))))))
 
-(defendpoint GET "/:id/metadata"
+(api/defendpoint GET "/:id/metadata"
   "Get metadata about a `Database`, including all of its `Tables` and `Fields`.
    Returns DB, fields, and field values."
   [id]
@@ -121,7 +123,7 @@
         fields (filter mi/can-read? (autocomplete-fields db-id prefix))]
     (autocomplete-results tables fields)))
 
-(defendpoint GET "/:id/autocomplete_suggestions"
+(api/defendpoint GET "/:id/autocomplete_suggestions"
   "Return a list of autocomplete suggestions for a given PREFIX.
    This is intened for use with the ACE Editor when the User is typing raw SQL.
    Suggestions include matching `Tables` and `Fields` in this `Database`.
@@ -130,7 +132,7 @@
    Fields are returned in the format `[field_name \"table_name base_type special_type\"]`"
   [id prefix]
   {prefix su/NonBlankString}
-  (read-check Database id)
+  (api/read-check Database id)
   (try
     (autocomplete-suggestions id prefix)
     (catch Throwable t
@@ -139,10 +141,10 @@
 
 ;;; ------------------------------------------------------------ GET /api/database/:id/fields ------------------------------------------------------------
 
-(defendpoint GET "/:id/fields"
+(api/defendpoint GET "/:id/fields"
   "Get a list of all `Fields` in `Database`."
   [id]
-  (read-check Database id)
+  (api/read-check Database id)
   (for [{:keys [id display_name table base_type special_type]} (filter mi/can-read? (-> (db/select [Field :id :display_name :table_id :base_type :special_type]
                                                                                                    :table_id        [:in (db/select-field :id Table, :db_id id)]
                                                                                                    :visibility_type [:not-in ["sensitive" "retired"]])
@@ -157,10 +159,10 @@
 
 ;;; ------------------------------------------------------------ GET /api/database/:id/idfields ------------------------------------------------------------
 
-(defendpoint GET "/:id/idfields"
+(api/defendpoint GET "/:id/idfields"
   "Get a list of all primary key `Fields` for `Database`."
   [id]
-  (read-check Database id)
+  (api/read-check Database id)
   (sort-by (comp str/lower-case :name :table) (filter mi/can-read? (-> (database/pk-fields {:id id})
                                                                          (hydrate :table)))))
 
@@ -195,13 +197,13 @@
                             (:name field)))]
     (contains? driver-props "ssl")))
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Add a new `Database`."
   [:as {{:keys [name engine details is_full_sync]} :body}]
   {name    su/NonBlankString
    engine  DBEngine
    details su/Map}
-  (check-superuser)
+  (api/check-superuser)
   ;; this function tries connecting over ssl and non-ssl to establish a connection
   ;; if it succeeds it returns the `details` that worked, otherwise it returns an error
   (let [try-connection   (fn [engine details]
@@ -219,7 +221,7 @@
                            (boolean is_full_sync))]
     (if-not (false? (:valid details-or-error))
       ;; no error, proceed with creation
-      (let-500 [new-db (db/insert! Database, :name name, :engine engine, :details details-or-error, :is_full_sync is_full_sync)]
+      (api/let-500 [new-db (db/insert! Database, :name name, :engine engine, :details details-or-error, :is_full_sync is_full_sync)]
         (events/publish-event! :database-create new-db)
         new-db)
       ;; failed to connect, return error
@@ -229,23 +231,23 @@
 
 ;;; ------------------------------------------------------------ POST /api/database/sample_dataset ------------------------------------------------------------
 
-(defendpoint POST "/sample_dataset"
+(api/defendpoint POST "/sample_dataset"
   "Add the sample dataset as a new `Database`."
   []
-  (check-superuser)
+  (api/check-superuser)
   (sample-data/add-sample-dataset!)
   (Database :is_sample true))
 
 ;;; ------------------------------------------------------------ PUT /api/database/:id ------------------------------------------------------------
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `Database`."
   [id :as {{:keys [name engine details is_full_sync description caveats points_of_interest]} :body}]
   {name    su/NonBlankString
    engine  DBEngine
    details su/Map}
-  (check-superuser)
-  (let-404 [database (Database id)]
+  (api/check-superuser)
+  (api/let-404 [database (Database id)]
     (let [details      (if-not (= protected-password (:password details))
                          details
                          (assoc details :password (get-in database [:details :password])))
@@ -257,14 +259,14 @@
         (do
           ;; TODO: is there really a reason to let someone change the engine on an existing database?
           ;;       that seems like the kind of thing that will almost never work in any practical way
-          (check-500 (db/update-non-nil-keys! Database id
-                       :name               name
-                       :engine             engine
-                       :details            details
-                       :is_full_sync       is_full_sync
-                       :description        description
-                       :caveats            caveats
-                       :points_of_interest points_of_interest)) ; TODO - this means one cannot unset the description. Does that matter?
+          (api/check-500 (db/update-non-nil-keys! Database id
+                           :name               name
+                           :engine             engine
+                           :details            details
+                           :is_full_sync       is_full_sync
+                           :description        description
+                           :caveats            caveats
+                           :points_of_interest points_of_interest)) ; TODO - this means one cannot unset the description. Does that matter?
           (events/publish-event! :database-update (Database id)))
         ;; failed to connect, return error
         {:status 400
@@ -272,25 +274,25 @@
 
 ;;; ------------------------------------------------------------ DELETE /api/database/:id ------------------------------------------------------------
 
-(defendpoint DELETE "/:id"
+(api/defendpoint DELETE "/:id"
   "Delete a `Database`."
   [id]
-  (let-404 [db (Database id)]
-    (write-check db)
+  (api/let-404 [db (Database id)]
+    (api/write-check db)
     (db/delete! Database :id id)
     (events/publish-event! :database-delete db))
-  generic-204-no-content)
+  api/generic-204-no-content)
 
 
 ;;; ------------------------------------------------------------ POST /api/database/:id/sync ------------------------------------------------------------
 
 ;; TODO - Shouldn't we just check for superuser status instead of write checking?
-(defendpoint POST "/:id/sync"
+(api/defendpoint POST "/:id/sync"
   "Update the metadata for this `Database`."
   [id]
   ;; just publish a message and let someone else deal with the logistics
-  (events/publish-event! :database-trigger-sync (write-check Database id))
+  (events/publish-event! :database-trigger-sync (api/write-check Database id))
   {:status :ok})
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index 0dae851c67f8b593f82f55715825106e9029bb05..20b0bd764305323dc10f38b8fd518212ee8de1ee 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -1,18 +1,16 @@
 (ns metabase.api.dataset
   "/api/dataset endpoints."
-  (:require [clojure.data.csv :as csv]
-            [cheshire.core :as json]
-            [compojure.core :refer [GET POST]]
-            [metabase.api.common :refer :all]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            (metabase.models [card :refer [Card]]
-                             [database :refer [Database]]
-                             [query :as query]
-                             [query-execution :refer [QueryExecution]])
-            [metabase.query-processor :as qp]
+  (:require [cheshire.core :as json]
+            [clojure.data.csv :as csv]
+            [compojure.core :refer [POST]]
+            [metabase
+             [query-processor :as qp]
+             [util :as u]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [database :refer [Database]]
+             [query :as query]]
             [metabase.query-processor.util :as qputil]
-            [metabase.util :as u]
             [metabase.util.schema :as su]))
 
 (def ^:private ^:const max-results-bare-rows
@@ -28,18 +26,18 @@
   {:max-results           max-results
    :max-results-bare-rows max-results-bare-rows})
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Execute a query and retrieve the results in the usual format."
   [:as {{:keys [database] :as body} :body}]
-  (read-check Database database)
+  (api/read-check Database database)
   ;; add sensible constraints for results limits on our query
   (let [query (assoc body :constraints default-query-constraints)]
-    (qp/dataset-query query {:executed-by *current-user-id*, :context :ad-hoc})))
+    (qp/dataset-query query {:executed-by api/*current-user-id*, :context :ad-hoc})))
 
-(defendpoint POST "/duration"
+(api/defendpoint POST "/duration"
   "Get historical query execution duration."
   [:as {{:keys [database], :as query} :body}]
-  (read-check Database database)
+  (api/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 (query/average-execution-time-ms (qputil/query-hash query))
@@ -76,21 +74,21 @@
     {:status 500
      :body   {:error (:error response)}}))
 
-(defendpoint POST "/csv"
+(api/defendpoint POST "/csv"
   "Execute a query and download the result data as a CSV file."
   [query]
   {query su/JSONString}
   (let [query (json/parse-string query keyword)]
-    (read-check Database (:database query))
-    (as-csv (qp/dataset-query (dissoc query :constraints) {:executed-by *current-user-id*, :context :csv-download}))))
+    (api/read-check Database (:database query))
+    (as-csv (qp/dataset-query (dissoc query :constraints) {:executed-by api/*current-user-id*, :context :csv-download}))))
 
-(defendpoint POST "/json"
+(api/defendpoint POST "/json"
   "Execute a query and download the result data as a JSON file."
   [query]
   {query su/JSONString}
   (let [query (json/parse-string query keyword)]
-    (read-check Database (:database query))
-    (as-json (qp/dataset-query (dissoc query :constraints) {:executed-by *current-user-id*, :context :json-download}))))
+    (api/read-check Database (:database query))
+    (as-json (qp/dataset-query (dissoc query :constraints) {:executed-by api/*current-user-id*, :context :json-download}))))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/email.clj b/src/metabase/api/email.clj
index e3452558f1a6064e179f7e120478142a2bef6c02..16ec9948ea618737182c9ce53e56c7788dd27d5a 100644
--- a/src/metabase/api/email.clj
+++ b/src/metabase/api/email.clj
@@ -1,11 +1,15 @@
 (ns metabase.api.email
   "/api/email endpoints"
-  (:require [clojure.tools.logging :as log]
-            [clojure.set :as set]
-            [compojure.core :refer [GET PUT DELETE POST]]
-            [metabase.api.common :refer :all]
-            [metabase.config :as config]
-            [metabase.email :as email]
+  (:require [clojure
+             [data :as data]
+             [set :as set]
+             [string :as string]]
+            [clojure.tools.logging :as log]
+            [compojure.core :refer [POST PUT]]
+            [metabase
+             [config :as config]
+             [email :as email]]
+            [metabase.api.common :as api]
             [metabase.models.setting :as setting]
             [metabase.util.schema :as su]))
 
@@ -51,11 +55,21 @@
         #".*"
         {:message "Sorry, something went wrong.  Please try again."}))))
 
-(defendpoint PUT "/"
+(defn humanize-email-corrections
+  "formats warnings when security settings are autocorrected"
+  [corrections]
+  (into {}
+        (mapv (fn [[k v]]
+                [k (format "%s was autocorrected to %s"
+                           (name (mb-to-smtp-settings k))
+                           (string/upper-case v))])
+              corrections)))
+
+(api/defendpoint PUT "/"
   "Update multiple `Settings` values.  You must be a superuser to do this."
   [:as {settings :body}]
   {settings su/Map}
-  (check-superuser)
+  (api/check-superuser)
   (let [email-settings (select-keys settings (keys mb-to-smtp-settings))
         smtp-settings  (-> (set/rename-keys email-settings mb-to-smtp-settings)
                            (assoc :port (Integer/parseInt (:email-smtp-port settings))))
@@ -63,21 +77,26 @@
                          ;; in normal conditions, validate connection
                          (email/test-smtp-connection smtp-settings)
                          ;; for unit testing just respond with a success message
-                         {:error :SUCCESS})]
+                         {:error :SUCCESS})
+        tested-settings  (merge settings (select-keys response [:port :security]))
+        [_ corrections _] (data/diff settings tested-settings)
+        properly-named-corrections (set/rename-keys corrections (set/map-invert mb-to-smtp-settings))
+        corrected-settings (merge email-settings properly-named-corrections)]
     (if (= :SUCCESS (:error response))
       ;; test was good, save our settings
-      (setting/set-many! email-settings)
+      (assoc (setting/set-many! corrected-settings)
+        :with-corrections (humanize-email-corrections properly-named-corrections))
       ;; test failed, return response message
       {:status 500
        :body   (humanize-error-messages response)})))
 
-(defendpoint POST "/test"
+(api/defendpoint POST "/test"
   "Send a test email. You must be a superuser to do this."
   []
-  (check-superuser)
+  (api/check-superuser)
   (let [response (email/send-message!
                    :subject      "Metabase Test Email"
-                   :recipients   [(:email @*current-user*)]
+                   :recipients   [(:email @api/*current-user*)]
                    :message-type :text
                    :message      "Your Metabase emails are working — hooray!")]
     (if (= :SUCCESS (:error response))
@@ -85,4 +104,4 @@
       {:status 500
        :body   (humanize-error-messages response)})))
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/embed.clj b/src/metabase/api/embed.clj
index 5f713e6212dad3fbfb7260e96c61e5bb5276cfcb..acf579a660a2c085e2bfacd97e2df8801dfdcc00 100644
--- a/src/metabase/api/embed.clj
+++ b/src/metabase/api/embed.clj
@@ -14,25 +14,26 @@
       {:resource {:question  <card-id>
                   :dashboard <dashboard-id>}
        :params   <params>}"
-  ;; TODO - switch resource.question back to resource.card
-  (:require (clojure [set :as set]
-                     [string :as str])
+  (:require [clojure
+             [set :as set]
+             [string :as str]]
             [clojure.tools.logging :as log]
             [compojure.core :refer [GET]]
             [medley.core :as m]
-            [schema.core :as s]
-            [toucan.db :as db]
-            (metabase.api [common :as api]
-                          [dataset :as dataset-api]
-                          [public :as public-api])
-            (metabase.models [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [dashboard-card :refer [DashboardCard]])
-            [metabase.models.setting :as setting]
+            [metabase.api
+             [common :as api]
+             [dataset :as dataset-api]
+             [public :as public-api]]
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [dashboard-card :refer [DashboardCard]]]
             [metabase.util :as u]
-            (metabase.util [embed :as eu]
-                           [schema :as su])))
-
+            [metabase.util
+             [embed :as eu]
+             [schema :as su]]
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 ;;; ------------------------------------------------------------ Param Checking ------------------------------------------------------------
 
@@ -229,6 +230,7 @@
   ([object]
    (api/check-embedding-enabled)
    (api/check-404 object)
+   (api/check-not-archived object)
    (api/check (:enable_embedding object)
      [400 "Embedding is not enabled for this object."])))
 
diff --git a/src/metabase/api/field.clj b/src/metabase/api/field.clj
index 4729dd7fb1d2e4ad1d065847eabd60308c45ee56..e1e0949e8e5ec761c6326e3ccaa9be0e6c89d5db 100644
--- a/src/metabase/api/field.clj
+++ b/src/metabase/api/field.clj
@@ -1,15 +1,16 @@
 (ns metabase.api.field
-  (:require [compojure.core :refer [GET PUT POST]]
-            [schema.core :as s]
-            [metabase.api.common :refer :all]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
+  (:require [compojure.core :refer [GET POST PUT]]
+            [metabase.api.common :as api]
             [metabase.db.metadata-queries :as metadata]
-            (metabase.models [field :refer [Field] :as field]
-                             [field-values :refer [FieldValues create-field-values-if-needed! field-should-have-field-values?]])
-            metabase.types
+            [metabase.models
+             [field :as field :refer [Field]]
+             [field-values :refer [create-field-values-if-needed! field-should-have-field-values? FieldValues]]]
             [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
 (def ^:private FieldType
   "Schema for a valid `Field` type."
@@ -21,14 +22,14 @@
   (apply s/enum (map name field/visibility-types)))
 
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Get `Field` with ID."
   [id]
-  (-> (read-check Field id)
+  (-> (api/read-check Field id)
       (hydrate [:table :db])))
 
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update `Field` with ID."
   [id :as {{:keys [caveats description display_name fk_target_field_id points_of_interest special_type visibility_type], :as body} :body}]
   {caveats            (s/maybe su/NonBlankString)
@@ -38,60 +39,56 @@
    points_of_interest (s/maybe su/NonBlankString)
    special_type       (s/maybe FieldType)
    visibility_type    (s/maybe FieldVisibilityType)}
-  (let [field (write-check Field id)]
-    (let [special_type       (keyword (get body :special_type (:special_type field)))
-          visibility_type    (or visibility_type (:visibility_type field))
-          ;; only let target field be set for :type/FK type fields, and if it's not in the payload then leave the current value
-          fk-target-field-id (when (isa? special_type :type/FK)
-                               (get body :fk_target_field_id (:fk_target_field_id field)))]
-      ;; validate that fk_target_field_id is a valid Field
-      ;; TODO - we should also check that the Field is within the same database as our field
-      (when fk-target-field-id
-        (checkp (db/exists? Field :id fk-target-field-id)
-          :fk_target_field_id "Invalid target field"))
-      ;; everything checks out, now update the field
-      (check-500 (db/update! Field id (merge {:caveats            caveats
-                                              :description        description
-                                              :fk_target_field_id fk_target_field_id
-                                              :points_of_interest points_of_interest
-                                              :special_type       special_type
-                                              :visibility_type    visibility_type}
-                                             (when display_name {:display_name display_name}))))
-      ;; return updated field
-      (Field id))))
+  (let [field              (api/write-check Field id)
+        special_type       (keyword (or special_type (:special_type field)))
+        ;; only let target field be set for :type/FK type fields, and if it's not in the payload then leave the current value
+        fk_target_field_id (when (isa? special_type :type/FK)
+                             (or fk_target_field_id (:fk_target_field_id field)))]
+    ;; validate that fk_target_field_id is a valid Field
+    ;; TODO - we should also check that the Field is within the same database as our field
+    (when fk_target_field_id
+      (api/checkp (db/exists? Field :id fk_target_field_id)
+        :fk_target_field_id "Invalid target field"))
+    ;; everything checks out, now update the field
+    (api/check-500 (db/update! Field id
+                     (u/select-keys-when (assoc body :fk_target_field_id fk_target_field_id)
+                       :present #{:caveats :description :fk_target_field_id :points_of_interest :special_type :visibility_type}
+                       :non-nil #{:display_name})))
+    ;; return updated field
+    (Field id)))
 
-(defendpoint GET "/:id/summary"
+(api/defendpoint GET "/:id/summary"
   "Get the count and distinct count of `Field` with ID."
   [id]
-  (let [field (read-check Field id)]
+  (let [field (api/read-check Field id)]
     [[:count     (metadata/field-count field)]
      [:distincts (metadata/field-distinct-count field)]]))
 
 
-(defendpoint GET "/:id/values"
+(api/defendpoint GET "/:id/values"
   "If `Field`'s special type derives from `type/Category`, or its base type is `type/Boolean`, return
    all distinct values of the field, and a map of human-readable values defined by the user."
   [id]
-  (let [field (read-check Field id)]
+  (let [field (api/read-check Field id)]
     (if-not (field-should-have-field-values? field)
       {:values {} :human_readable_values {}}
       (create-field-values-if-needed! field))))
 
 
 ;; TODO - not sure this is used anymore
-(defendpoint POST "/:id/value_map_update"
+(api/defendpoint POST "/:id/value_map_update"
   "Update the human-readable values for a `Field` whose special type is `category`/`city`/`state`/`country`
    or whose base type is `type/Boolean`."
   [id :as {{:keys [values_map]} :body}]
   {values_map su/Map}
-  (let [field (write-check Field id)]
-    (check (field-should-have-field-values? field)
+  (let [field (api/write-check Field id)]
+    (api/check (field-should-have-field-values? field)
       [400 "You can only update the mapped values of a Field whose 'special_type' is 'category'/'city'/'state'/'country' or whose 'base_type' is 'type/Boolean'."])
     (if-let [field-values-id (db/select-one-id FieldValues, :field_id id)]
-      (check-500 (db/update! FieldValues field-values-id
-                   :human_readable_values values_map))
+      (api/check-500 (db/update! FieldValues field-values-id
+                       :human_readable_values values_map))
       (create-field-values-if-needed! field values_map)))
   {:status :success})
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/geojson.clj b/src/metabase/api/geojson.clj
index 7c6e6a3c61ccd4ae4c1786f5a8e47b9a947b17fc..19494cd7be689705ed1926c8444477f5f043a851 100644
--- a/src/metabase/api/geojson.clj
+++ b/src/metabase/api/geojson.clj
@@ -1,12 +1,12 @@
 (ns metabase.api.geojson
-  (:require [clojure.java.io :as io]
-            [cheshire.core :as json]
-            [compojure.core :refer [defroutes GET]]
-            [schema.core :as s]
-            [metabase.api.common :refer :all]
-            [metabase.models.setting :refer [defsetting], :as setting]
+  (:require [cheshire.core :as json]
+            [clojure.java.io :as io]
+            [compojure.core :refer [GET]]
+            [metabase.api.common :refer [defendpoint define-routes]]
+            [metabase.models.setting :as setting :refer [defsetting]]
             [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]))
 
 (defn- valid-json?
   "Does this URL-OR-RESOURCE point to valid JSON?
diff --git a/src/metabase/api/getting_started.clj b/src/metabase/api/getting_started.clj
index 8a62d517749ca8bc181b0e2c391e0e183745c83f..40c0c2b6ec3a8840102d7660f9957fe6b75313d1 100644
--- a/src/metabase/api/getting_started.clj
+++ b/src/metabase/api/getting_started.clj
@@ -1,10 +1,11 @@
 (ns metabase.api.getting-started
   (:require [compojure.core :refer [GET]]
             [medley.core :as m]
-            [metabase.api.common :refer :all]
-            [toucan.db :as db]
-            (metabase.models [interface :as mi]
-                             [setting :refer [defsetting]])))
+            [metabase.api.common :as api]
+            [metabase.models
+             [interface :as mi]
+             [setting :refer [defsetting]]]
+            [toucan.db :as db]))
 
 (defsetting getting-started-things-to-know
   "'Some things to know' text field for the Getting Started guide.")
@@ -16,7 +17,7 @@
   "Email of somebody users can contact for help in the Getting Started guide.")
 
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Fetch basic info for the Getting Started guide."
   []
   (let [metric-ids  (map :id (filter mi/can-read? (db/select ['Metric :table_id :id]     :show_in_getting_started true, {:order-by [:%lower.name]})))
@@ -37,4 +38,4 @@
                                                                   (db/select ['MetricImportantField :field_id :metric_id]
                                                                     :metric_id [:in metric-ids]))))}))
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/label.clj b/src/metabase/api/label.clj
index 7a272fe01285d3a7f214847cdc7307d8b6067ff2..d585ae04fcdb3c19458be3f7c71ad1e0e669c7bb 100644
--- a/src/metabase/api/label.clj
+++ b/src/metabase/api/label.clj
@@ -1,13 +1,13 @@
 (ns ^:deprecated metabase.api.label
   "`/api/label` endpoints."
   (:require [clojure.tools.logging :as log]
-            [compojure.core :refer [GET POST DELETE PUT]]
-            [schema.core :as s]
-            [metabase.api.common :refer [defendpoint define-routes write-check], :as api]
-            [toucan.db :as db]
+            [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase.api.common :as api :refer [defendpoint define-routes write-check]]
             [metabase.models.label :refer [Label]]
             [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 (defn warn-about-labels-being-deprecated
   "Print a warning message about Labels-related endpoints being deprecated."
diff --git a/src/metabase/api/metric.clj b/src/metabase/api/metric.clj
index fcee3655552f5755b072f3df84fd00512e611d4c..772382aba0993dc9b099fb4cbabe7df577c16e6f 100644
--- a/src/metabase/api/metric.clj
+++ b/src/metabase/api/metric.clj
@@ -1,35 +1,34 @@
 (ns metabase.api.metric
   "/api/metric endpoints."
   (:require [clojure.data :as data]
-            [clojure.tools.logging :as log]
-            [compojure.core :refer [defroutes GET PUT POST DELETE]]
-            [schema.core :as s]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            [metabase.api.common :refer :all]
-            (metabase.models [interface :as mi]
-                             [metric :refer [Metric], :as metric]
-                             [revision :as revision]
-                             [table :refer [Table]])
-            [metabase.util.schema :as su]))
-
-
-(defendpoint POST "/"
+            [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [interface :as mi]
+             [metric :as metric :refer [Metric]]
+             [revision :as revision]
+             [table :refer [Table]]]
+            [metabase.util.schema :as su]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
+
+(api/defendpoint POST "/"
   "Create a new `Metric`."
   [:as {{:keys [name description table_id definition]} :body}]
   {name       su/NonBlankString
    table_id   su/IntGreaterThanZero
    definition su/Map}
-  (check-superuser)
-  (write-check Table table_id)
-  (check-500 (metric/create-metric! table_id name description *current-user-id* definition)))
+  (api/check-superuser)
+  (api/write-check Table table_id)
+  (api/check-500 (metric/create-metric! table_id name description api/*current-user-id* definition)))
 
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Fetch `Metric` with ID."
   [id]
-  (check-superuser)
-  (read-check (metric/retrieve-metric id)))
+  (api/check-superuser)
+  (api/read-check (metric/retrieve-metric id)))
 
 (defn- add-db-ids
   "Add `:database_id` fields to METRICS by looking them up from their `:table_id`."
@@ -39,42 +38,36 @@
       (for [metric metrics]
         (assoc metric :database_id (table-id->db-id (:table_id metric)))))))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Fetch *all* `Metrics`."
   [id]
-  (filter mi/can-read? (-> (db/select Metric, :is_active true, {:order-by [:%lower.name]})
-                           (hydrate :creator)
-                           add-db-ids)))
+  (as-> (db/select Metric, :is_active true, {:order-by [:%lower.name]}) <>
+    (hydrate <> :creator)
+    (add-db-ids <>)
+    (filter mi/can-read? <>)))
 
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `Metric` with ID."
-  [id :as {{:keys [name description caveats points_of_interest how_is_this_calculated show_in_getting_started definition revision_message]} :body}]
+  [id :as {{:keys [definition name revision_message], :as body} :body}]
   {name             su/NonBlankString
    revision_message su/NonBlankString
    definition       su/Map}
-  (check-superuser)
-  (write-check Metric id)
+  (api/check-superuser)
+  (api/write-check Metric id)
   (metric/update-metric!
-    {:id                      id
-     :name                    name
-     :description             description
-     :caveats                 caveats
-     :points_of_interest      points_of_interest
-     :how_is_this_calculated  how_is_this_calculated
-     :show_in_getting_started show_in_getting_started
-     :definition              definition
-     :revision_message        revision_message}
-    *current-user-id*))
-
-(defendpoint PUT "/:id/important_fields"
+   (assoc (select-keys body #{:caveats :definition :description :how_is_this_calculated :name :points_of_interest :revision_message :show_in_getting_started})
+     :id id)
+   api/*current-user-id*))
+
+(api/defendpoint PUT "/:id/important_fields"
   "Update the important `Fields` for a `Metric` with ID.
    (This is used for the Getting Started guide)."
   [id :as {{:keys [important_field_ids]} :body}]
   {important_field_ids [su/IntGreaterThanZero]}
-  (check-superuser)
-  (write-check Metric id)
-  (check (<= (count important_field_ids) 3)
+  (api/check-superuser)
+  (api/write-check Metric id)
+  (api/check (<= (count important_field_ids) 3)
     [400 "A Metric can have a maximum of 3 important fields."])
   (let [[fields-to-remove fields-to-add] (data/diff (set (db/select-field :field_id 'MetricImportantField :metric_id id))
                                                     (set important_field_ids))]
@@ -88,35 +81,35 @@
     {:success true}))
 
 
-(defendpoint DELETE "/:id"
+(api/defendpoint DELETE "/:id"
   "Delete a `Metric`."
   [id revision_message]
   {revision_message su/NonBlankString}
-  (check-superuser)
-  (write-check Metric id)
-  (metric/delete-metric! id *current-user-id* revision_message)
+  (api/check-superuser)
+  (api/write-check Metric id)
+  (metric/delete-metric! id api/*current-user-id* revision_message)
   {:success true}) ; TODO - why doesn't this return a 204 'No Content'?
 
 
-(defendpoint GET "/:id/revisions"
+(api/defendpoint GET "/:id/revisions"
   "Fetch `Revisions` for `Metric` with ID."
   [id]
-  (check-superuser)
-  (write-check Metric id)
+  (api/check-superuser)
+  (api/write-check Metric id)
   (revision/revisions+details Metric id))
 
 
-(defendpoint POST "/:id/revert"
+(api/defendpoint POST "/:id/revert"
   "Revert a `Metric` to a prior `Revision`."
   [id :as {{:keys [revision_id]} :body}]
   {revision_id su/IntGreaterThanZero}
-  (check-superuser)
-  (write-check Metric id)
+  (api/check-superuser)
+  (api/write-check Metric id)
   (revision/revert!
     :entity      Metric
     :id          id
-    :user-id     *current-user-id*
+    :user-id     api/*current-user-id*
     :revision-id revision_id))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/notify.clj b/src/metabase/api/notify.clj
index 8e70f9f665bb0fed18d40c9dbc6e29db874569f0..894a441fe5278e905a5589ba2da148edfac7966d 100644
--- a/src/metabase/api/notify.clj
+++ b/src/metabase/api/notify.clj
@@ -1,19 +1,17 @@
 (ns metabase.api.notify
   "/api/notify/* endpoints which receive inbound etl server notifications."
   (:require [compojure.core :refer [POST]]
-            [metabase.api.common :refer :all]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            (metabase.models [database :refer [Database]]
-                             [table :refer [Table]])
+            [metabase.api.common :as api]
+            [metabase.models
+             [database :refer [Database]]
+             [table :refer [Table]]]
             [metabase.sync-database :as sync-database]))
 
-
-(defendpoint POST "/db/:id"
+(api/defendpoint POST "/db/:id"
   "Notification about a potential schema change to one of our `Databases`.
   Caller can optionally specify a `:table_id` or `:table_name` in the body to limit updates to a single `Table`."
   [id :as {{:keys [table_id table_name]} :body}]
-  (let-404 [database (Database id)]
+  (api/let-404 [database (Database id)]
     (cond
       table_id (when-let [table (Table :db_id id, :id (int table_id))]
                  (future (sync-database/sync-table! table)))
@@ -23,4 +21,4 @@
   {:success true})
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/permissions.clj b/src/metabase/api/permissions.clj
index 0179b9f9d28997d83d9dfd3e65539d8f06fd8f04..6a5eb25b84bfde9b98f85e842b3d2b7bf9b0707b 100644
--- a/src/metabase/api/permissions.clj
+++ b/src/metabase/api/permissions.clj
@@ -1,17 +1,18 @@
 (ns metabase.api.permissions
   "/api/permissions endpoints."
-  (:require [compojure.core :refer [GET POST PUT DELETE]]
-            [metabase.api.common :refer :all]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            [metabase.metabot :as metabot]
-            (metabase.models [database :as database]
-                             [permissions :refer [Permissions], :as perms]
-                             [permissions-group :refer [PermissionsGroup], :as group]
-                             [permissions-group-membership :refer [PermissionsGroupMembership]]
-                             [table :refer [Table]])
-            [metabase.util :as u]
-            [metabase.util.schema :as su]))
+  (:require [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase
+             [metabot :as metabot]
+             [util :as u]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [permissions :as perms]
+             [permissions-group :as group :refer [PermissionsGroup]]
+             [permissions-group-membership :refer [PermissionsGroupMembership]]]
+            [metabase.util.schema :as su]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
 ;;; +------------------------------------------------------------------------------------------------------------------------------------------------------+
 ;;; |                                                             PERMISSIONS GRAPH ENDPOINTS                                                              |
@@ -50,14 +51,14 @@
 
 ;;; ---------------------------------------- Endpoints ----------------------------------------
 
-(defendpoint GET "/graph"
+(api/defendpoint GET "/graph"
   "Fetch a graph of all Permissions."
   []
-  (check-superuser)
+  (api/check-superuser)
   (perms/graph))
 
 
-(defendpoint PUT "/graph"
+(api/defendpoint PUT "/graph"
   "Do a batch update of Permissions by passing in a modified graph. This should return the same graph,
    in the same format, that you got from `GET /api/permissions/graph`, with any changes made in the wherever neccesary.
    This modified graph must correspond to the `PermissionsGraph` schema.
@@ -68,7 +69,7 @@
    and make desired changes to that."
   [:as {body :body}]
   {body su/Map}
-  (check-superuser)
+  (api/check-superuser)
   (perms/update-graph! (dejsonify-graph body))
   (perms/graph))
 
@@ -97,80 +98,80 @@
                  [:not= :id (u/get-id (group/metabot))])
      :order-by [:%lower.name]}))
 
-(defendpoint GET "/group"
+(api/defendpoint GET "/group"
   "Fetch all `PermissionsGroups`, including a count of the number of `:members` in that group."
   []
-  (check-superuser)
+  (api/check-superuser)
   (let [group-id->members (group-id->num-members)]
     (for [group (ordered-groups)]
       (assoc group :members (or (group-id->members (u/get-id group))
                                 0)))))
 
-(defendpoint GET "/group/:id"
+(api/defendpoint GET "/group/:id"
   "Fetch the details for a certain permissions group."
   [id]
-  (check-superuser)
+  (api/check-superuser)
   (-> (PermissionsGroup id)
       (hydrate :members)))
 
-(defendpoint POST "/group"
+(api/defendpoint POST "/group"
   "Create a new `PermissionsGroup`."
   [:as {{:keys [name]} :body}]
   {name su/NonBlankString}
-  (check-superuser)
+  (api/check-superuser)
   (db/insert! PermissionsGroup
     :name name))
 
-(defendpoint PUT "/group/:group-id"
+(api/defendpoint PUT "/group/:group-id"
   "Update the name of a `PermissionsGroup`."
   [group-id :as {{:keys [name]} :body}]
   {name su/NonBlankString}
-  (check-superuser)
-  (check-404 (db/exists? PermissionsGroup :id group-id))
+  (api/check-superuser)
+  (api/check-404 (db/exists? PermissionsGroup :id group-id))
   (db/update! PermissionsGroup group-id
     :name name)
   ;; return the updated group
   (PermissionsGroup group-id))
 
-(defendpoint DELETE "/group/:group-id"
+(api/defendpoint DELETE "/group/:group-id"
   "Delete a specific `PermissionsGroup`."
   [group-id]
-  (check-superuser)
+  (api/check-superuser)
   (db/delete! PermissionsGroup :id group-id)
-  generic-204-no-content)
+  api/generic-204-no-content)
 
 
 ;;; ---------------------------------------- Group Membership Endpoints ----------------------------------------
 
-(defendpoint GET "/membership"
+(api/defendpoint GET "/membership"
   "Fetch a map describing the group memberships of various users.
    This map's format is:
 
     {<user-id> [{:membership_id <id>
                  :group_id      <id>}]}"
   []
-  (check-superuser)
+  (api/check-superuser)
   (group-by :user_id (db/select [PermissionsGroupMembership [:id :membership_id] :group_id :user_id])))
 
-(defendpoint POST "/membership"
+(api/defendpoint POST "/membership"
   "Add a `User` to a `PermissionsGroup`. Returns updated list of members belonging to the group."
   [:as {{:keys [group_id user_id]} :body}]
   {group_id su/IntGreaterThanZero
    user_id  su/IntGreaterThanZero}
-  (check-superuser)
+  (api/check-superuser)
   (db/insert! PermissionsGroupMembership
     :group_id group_id
     :user_id  user_id)
   ;; TODO - it's a bit silly to return the entire list of members for the group, just return the newly created one and let the frontend add it ass appropriate
   (group/members {:id group_id}))
 
-(defendpoint DELETE "/membership/:id"
+(api/defendpoint DELETE "/membership/:id"
   "Remove a User from a PermissionsGroup (delete their membership)."
   [id]
-  (check-superuser)
-  (check-404 (db/exists? PermissionsGroupMembership :id id))
+  (api/check-superuser)
+  (api/check-404 (db/exists? PermissionsGroupMembership :id id))
   (db/delete! PermissionsGroupMembership :id id)
-  generic-204-no-content)
+  api/generic-204-no-content)
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/public.clj b/src/metabase/api/public.clj
index 710115f56295c7691879467e5dc45a3f8069ce95..0d16c542c99a1ab67973deaa98fa12ffe55c03b9 100644
--- a/src/metabase/api/public.clj
+++ b/src/metabase/api/public.clj
@@ -2,30 +2,53 @@
   "Metabase API endpoints for viewing publically-accessible Cards and Dashboards."
   (:require [cheshire.core :as json]
             [compojure.core :refer [GET]]
+            [metabase
+             [query-processor :as qp]
+             [util :as u]]
+            [metabase.api
+             [card :as card-api]
+             [common :as api]
+             [dataset :as dataset-api]]
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [dashboard-card :refer [DashboardCard]]
+             [dashboard-card-series :refer [DashboardCardSeries]]
+             [field-values :refer [FieldValues]]]
+            [metabase.query-processor.expand :as ql]
+            [metabase.util
+             [embed :as embed]
+             [schema :as su]]
             [schema.core :as s]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            (metabase.api [card :as card-api]
-                          [common :as api]
-                          [dataset :as dataset-api])
-            (metabase.models [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [dashboard-card :refer [DashboardCard]]
-                             [dashboard-card-series :refer [DashboardCardSeries]]
-                             [field-values :refer [FieldValues]])
-            [metabase.public-settings :as public-settings]
-            [metabase.query-processor :as qp]
-            (metabase.query-processor [expand :as ql]
-                                      interface)
-            [metabase.util :as u]
-            [metabase.util.schema :as su]
-            [metabase.util.embed :as embed])
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]])
   (:import metabase.query_processor.interface.FieldPlaceholder))
 
 (def ^:private ^:const ^Integer default-embed-max-height 800)
 (def ^:private ^:const ^Integer default-embed-max-width 1024)
 
 
+;;; ------------------------------------------------------------ Param Resolution Stuff Used For Both Card & Dashboards ------------------------------------------------------------
+
+(defn- field-form->id
+  "Expand a `field-id` or `fk->` FORM and return the ID of the Field it references.
+
+     (field-form->id [:field-id 100])  ; -> 100"
+  [field-form]
+  (when-let [field-placeholder (u/ignore-exceptions (ql/expand-ql-sexpr field-form))]
+    (when (instance? FieldPlaceholder field-placeholder)
+      (:field-id field-placeholder))))
+
+(defn- field-ids->param-field-values
+  "Given a collection of PARAM-FIELD-IDS return a map of FieldValues for the Fields they reference.
+   This map is returned by various endpoints as `:param_values`."
+  [param-field-ids]
+  (when (seq param-field-ids)
+    (u/key-by :field_id (db/select [FieldValues :values :human_readable_values :field_id]
+                          :field_id [:in param-field-ids]))))
+
+
 ;;; ------------------------------------------------------------ Public Cards ------------------------------------------------------------
 
 (defn- remove-card-non-public-fields
@@ -33,12 +56,27 @@
   [card]
   (u/select-nested-keys card [:id :name :description :display :visualization_settings [:dataset_query :type [:native :template_tags]]]))
 
+(defn- card->template-tag-field-ids
+  "Return a set of Field IDs referenced in template tag parameters in CARD."
+  [card]
+  (set (for [[_ {dimension :dimension}] (get-in card [:dataset_query :native :template_tags])
+             :when                      dimension
+             :let                       [field-id (field-form->id dimension)]
+             :when                      field-id]
+         field-id)))
+
+(defn- add-card-param-values
+  "Add FieldValues for any Fields referenced in CARD's `:template_tags`."
+  [card]
+  (assoc card :param_values (field-ids->param-field-values (card->template-tag-field-ids card))))
+
 (defn public-card
   "Return a public Card matching key-value CONDITIONS, removing all fields that should not be visible to the general public.
    Throws a 404 if the Card doesn't exist."
   [& conditions]
   (-> (api/check-404 (apply db/select-one [Card :id :dataset_query :description :display :name :visualization_settings], :archived false, conditions))
-      remove-card-non-public-fields))
+      remove-card-non-public-fields
+      add-card-param-values))
 
 (defn- card-with-uuid [uuid] (public-card :public_uuid uuid))
 
@@ -93,15 +131,6 @@
 ;; TODO - This logic seems too complicated for a one-off custom response format. Simplification would be nice, as would potentially
 ;;        moving some of this logic into a shared module
 
-(defn- field-form->id
-  "Expand a `field-id` or `fk->` FORM and return the ID of the Field it references.
-
-     (field-form->id [:field-id 100])  ; -> 100"
-  [field-form]
-  (when-let [field-placeholder (u/ignore-exceptions (ql/expand-ql-sexpr field-form))]
-    (when (instance? FieldPlaceholder field-placeholder)
-      (:field-id field-placeholder))))
-
 (defn- template-tag->field-form
   "Fetch the `field-id` or `fk->` form from DASHCARD referenced by TEMPLATE-TAG.
 
@@ -133,9 +162,7 @@
   "Return a map of Field ID to FieldValues (if any) for any Fields referenced by Cards in DASHBOARD,
    or `nil` if none are referenced or none of them have FieldValues."
   [dashboard]
-  (when-let [param-field-ids (dashboard->param-field-ids dashboard)]
-    (u/key-by :field_id (db/select [FieldValues :values :human_readable_values :field_id]
-                          :field_id [:in param-field-ids]))))
+  (field-ids->param-field-values (dashboard->param-field-ids dashboard)))
 
 (defn- add-field-values-for-parameters
   "Add a `:param_values` map containing FieldValues for the parameter Fields in the DASHBOARD."
@@ -146,7 +173,7 @@
   "Return a public Dashboard matching key-value CONDITIONS, removing all fields that should not be visible to the general public.
    Throws a 404 if the Dashboard doesn't exist."
   [& conditions]
-  (-> (api/check-404 (apply db/select-one [Dashboard :name :description :id :parameters] conditions))
+  (-> (api/check-404 (apply db/select-one [Dashboard :name :description :id :parameters], :archived false, conditions))
       (hydrate [:ordered_cards :card :series])
       add-field-values-for-parameters
       (update :ordered_cards (fn [dashcards]
@@ -185,7 +212,7 @@
   [uuid card-id parameters]
   {parameters (s/maybe su/JSONString)}
   (api/check-public-sharing-enabled)
-  (public-dashcard-results (api/check-404 (db/select-one-id Dashboard :public_uuid uuid)) card-id parameters))
+  (public-dashcard-results (api/check-404 (db/select-one-id Dashboard :public_uuid uuid, :archived false)) card-id parameters))
 
 
 (api/defendpoint GET "/oembed"
diff --git a/src/metabase/api/pulse.clj b/src/metabase/api/pulse.clj
index df9bf545f23dcfb09ec7a0c5e928170e42ed9e81..71b05be793010d1acfe379a507ecc3b004c8f2cb 100644
--- a/src/metabase/api/pulse.clj
+++ b/src/metabase/api/pulse.clj
@@ -1,27 +1,27 @@
 (ns metabase.api.pulse
   "/api/pulse endpoints."
-  (:require [compojure.core :refer [defroutes GET PUT POST DELETE]]
+  (:require [compojure.core :refer [DELETE GET POST PUT]]
             [hiccup.core :refer [html]]
-            [schema.core :as s]
-            [metabase.api.common :refer :all]
-            [toucan.db :as db]
-            [metabase.email :as email]
-            [metabase.events :as events]
+            [metabase
+             [email :as email]
+             [events :as events]
+             [pulse :as p]
+             [query-processor :as qp]
+             [util :as u]]
+            [metabase.api.common :as api]
             [metabase.integrations.slack :as slack]
-            (metabase.models [card :refer [Card]]
-                             [database :refer [Database]]
-                             [interface :as mi]
-                             [pulse :refer [Pulse retrieve-pulse] :as pulse]
-                             [pulse-channel :refer [channel-types]])
-            [metabase.query-processor :as qp]
-            [metabase.pulse :as p]
+            [metabase.models
+             [card :refer [Card]]
+             [interface :as mi]
+             [pulse :as pulse :refer [Pulse]]
+             [pulse-channel :refer [channel-types]]]
             [metabase.pulse.render :as render]
-            [metabase.util :as u]
-            [metabase.util.schema :as su])
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db])
   (:import java.io.ByteArrayInputStream))
 
-
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Fetch all `Pulses`"
   []
   (for [pulse (pulse/retrieve-pulses)
@@ -35,9 +35,9 @@
 (defn- check-card-read-permissions [cards]
   (doseq [{card-id :id} cards]
     (assert (integer? card-id))
-    (read-check Card card-id)))
+    (api/read-check Card card-id)))
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Create a new `Pulse`."
   [:as {{:keys [name cards channels skip_if_empty]} :body}]
   {name          su/NonBlankString
@@ -45,24 +45,24 @@
    channels      (su/non-empty [su/Map])
    skip_if_empty s/Bool}
   (check-card-read-permissions cards)
-  (check-500 (pulse/create-pulse! name *current-user-id* (map u/get-id cards) channels skip_if_empty)))
+  (api/check-500 (pulse/create-pulse! name api/*current-user-id* (map u/get-id cards) channels skip_if_empty)))
 
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Fetch `Pulse` with ID."
   [id]
-  (read-check (pulse/retrieve-pulse id)))
+  (api/read-check (pulse/retrieve-pulse id)))
 
 
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `Pulse` with ID."
   [id :as {{:keys [name cards channels skip_if_empty]} :body}]
   {name          su/NonBlankString
    cards         (su/non-empty [su/Map])
    channels      (su/non-empty [su/Map])
    skip_if_empty s/Bool}
-  (write-check Pulse id)
+  (api/write-check Pulse id)
   (check-card-read-permissions cards)
   (pulse/update-pulse! {:id             id
                         :name           name
@@ -72,16 +72,16 @@
   (pulse/retrieve-pulse id))
 
 
-(defendpoint DELETE "/:id"
+(api/defendpoint DELETE "/:id"
   "Delete a `Pulse`."
   [id]
-  (let-404 [pulse (Pulse id)]
+  (api/let-404 [pulse (Pulse id)]
     (db/delete! Pulse :id id)
-    (events/publish-event! :pulse-delete (assoc pulse :actor_id *current-user-id*)))
-  generic-204-no-content)
+    (events/publish-event! :pulse-delete (assoc pulse :actor_id api/*current-user-id*)))
+  api/generic-204-no-content)
 
 
-(defendpoint GET "/form_input"
+(api/defendpoint GET "/form_input"
   "Provides relevant configuration information and user choices for creating/updating `Pulses`."
   []
   (let [chan-types (-> channel-types
@@ -100,20 +100,20 @@
                    (catch Throwable e
                      (assoc-in chan-types [:slack :error] (.getMessage e)))))}))
 
-(defendpoint GET "/preview_card/:id"
+(api/defendpoint GET "/preview_card/:id"
   "Get HTML rendering of a `Card` with ID."
   [id]
-  (let [card   (read-check Card id)
-        result (qp/dataset-query (:dataset_query card) {:executed-by *current-user-id*, :context :pulse, :card-id id})]
+  (let [card   (api/read-check Card id)
+        result (qp/dataset-query (:dataset_query card) {:executed-by api/*current-user-id*, :context :pulse, :card-id id})]
     {:status 200, :body (html [:html [:body {:style "margin: 0;"} (binding [render/*include-title* true
                                                                             render/*include-buttons* true]
                                                                     (render/render-pulse-card card result))]])}))
 
-(defendpoint GET "/preview_card_info/:id"
+(api/defendpoint GET "/preview_card_info/:id"
   "Get JSON object containing HTML rendering of a `Card` with ID and other information."
   [id]
-  (let [card      (read-check Card id)
-        result    (qp/dataset-query (:dataset_query card) {:executed-by *current-user-id*, :context :pulse, :card-id id})
+  (let [card      (api/read-check Card id)
+        result    (qp/dataset-query (:dataset_query card) {:executed-by api/*current-user-id*, :context :pulse, :card-id id})
         data      (:data result)
         card-type (render/detect-pulse-card-type card data)
         card-html (html (binding [render/*include-title* true]
@@ -123,16 +123,16 @@
      :pulse_card_html card-html
      :row_count       (:row_count result)}))
 
-(defendpoint GET "/preview_card_png/:id"
+(api/defendpoint GET "/preview_card_png/:id"
   "Get PNG rendering of a `Card` with ID."
   [id]
-  (let [card   (read-check Card id)
-        result (qp/dataset-query (:dataset_query card) {:executed-by *current-user-id*, :context :pulse, :card-id id})
+  (let [card   (api/read-check Card id)
+        result (qp/dataset-query (:dataset_query card) {:executed-by api/*current-user-id*, :context :pulse, :card-id id})
         ba     (binding [render/*include-title* true]
                  (render/render-pulse-card-to-png card result))]
     {:status 200, :headers {"Content-Type" "image/png"}, :body (ByteArrayInputStream. ba)}))
 
-(defendpoint POST "/test"
+(api/defendpoint POST "/test"
   "Test send an unsaved pulse."
   [:as {{:keys [name cards channels skip_if_empty] :as body} :body}]
   {name          su/NonBlankString
@@ -143,4 +143,4 @@
   (p/send-pulse! body)
   {:ok true})
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/revision.clj b/src/metabase/api/revision.clj
index 7da50011885b02e3566183a1208b749b988fea08..14a4479381eaa25017f70fe3334894a3de785fce 100644
--- a/src/metabase/api/revision.clj
+++ b/src/metabase/api/revision.clj
@@ -1,12 +1,14 @@
 (ns metabase.api.revision
   (:require [compojure.core :refer [GET POST]]
+            [metabase.api
+             [card :as card-api]
+             [common :as api]]
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [revision :as revision :refer [Revision]]]
             [schema.core :as s]
-            (metabase.api [card :as card-api]
-                          [common :refer :all])
-            [toucan.db :as db]
-            (metabase.models [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [revision :as revision, :refer [Revision]])))
+            [toucan.db :as db]))
 
 (def ^:private ^:const name->entity
   {"card"      Card
@@ -16,20 +18,20 @@
   "Schema for a valid revisionable entity name."
   (apply s/enum (keys name->entity)))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Get revisions of an object."
   [entity id]
   {entity Entity, id s/Int}
   (let [entity (name->entity entity)]
-    (check-404 (db/exists? entity :id id))
+    (api/check-404 (db/exists? entity :id id))
     (revision/revisions+details entity id)))
 
-(defendpoint POST "/revert"
+(api/defendpoint POST "/revert"
   "Revert an object to a prior revision."
   [:as {{:keys [entity id revision_id]} :body}]
   {entity Entity, id s/Int, revision_id s/Int}
   (let [entity   (name->entity entity)
-        revision (check-404 (Revision :model (:name entity), :model_id id, :id revision_id))]
+        revision (api/check-404 (Revision :model (:name entity), :model_id id, :id revision_id))]
     ;; if reverting a Card, make sure we have *data* permissions to run the query we're reverting to
     (when (= entity Card)
       (card-api/check-data-permissions-for-query (get-in revision [:object :dataset_query])))
@@ -37,7 +39,7 @@
     (revision/revert!
       :entity      entity
       :id          id
-      :user-id     *current-user-id*
+      :user-id     api/*current-user-id*
       :revision-id revision_id)))
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj
index 20ed2d16ff23ed87ca3a8d8bc7fae9f68015b08f..78e16f295229f6e846b8a0fb1a97f1b0805395cf 100644
--- a/src/metabase/api/routes.clj
+++ b/src/metabase/api/routes.clj
@@ -1,34 +1,36 @@
 (ns metabase.api.routes
-  (:require [compojure.core :refer [context defroutes GET]]
-            [compojure.route :as route]
-            (metabase.api [activity :as activity]
-                          [card :as card]
-                          [collection :as collection]
-                          [dashboard :as dashboard]
-                          [database :as database]
-                          [dataset :as dataset]
-                          [email :as email]
-                          [embed :as embed]
-                          [field :as field]
-                          [getting-started :as getting-started]
-                          [geojson :as geojson]
-                          [label :as label]
-                          [metric :as metric]
-                          [notify :as notify]
-                          [permissions :as permissions]
-                          [preview-embed :as preview-embed]
-                          [public :as public]
-                          [pulse :as pulse]
-                          [revision :as revision]
-                          [segment :as segment]
-                          [session :as session]
-                          [setting :as setting]
-                          [setup :as setup]
-                          [slack :as slack]
-                          [table :as table]
-                          [tiles :as tiles]
-                          [user :as user]
-                          [util :as util])
+  (:require [compojure
+             [core :refer [context defroutes]]
+             [route :as route]]
+            [metabase.api
+             [activity :as activity]
+             [card :as card]
+             [collection :as collection]
+             [dashboard :as dashboard]
+             [database :as database]
+             [dataset :as dataset]
+             [email :as email]
+             [embed :as embed]
+             [field :as field]
+             [geojson :as geojson]
+             [getting-started :as getting-started]
+             [label :as label]
+             [metric :as metric]
+             [notify :as notify]
+             [permissions :as permissions]
+             [preview-embed :as preview-embed]
+             [public :as public]
+             [pulse :as pulse]
+             [revision :as revision]
+             [segment :as segment]
+             [session :as session]
+             [setting :as setting]
+             [setup :as setup]
+             [slack :as slack]
+             [table :as table]
+             [tiles :as tiles]
+             [user :as user]
+             [util :as util]]
             [metabase.middleware :as middleware]))
 
 (def ^:private +generic-exceptions
diff --git a/src/metabase/api/segment.clj b/src/metabase/api/segment.clj
index f45f98007ae6b76beb9f8f6107301b3ad5dc31be..9a644069ad7de529ddd33919899fb68577bbbda5 100644
--- a/src/metabase/api/segment.clj
+++ b/src/metabase/api/segment.clj
@@ -1,91 +1,85 @@
 (ns metabase.api.segment
   "/api/segment endpoints."
-  (:require [compojure.core :refer [defroutes GET PUT POST DELETE]]
-            [schema.core :as s]
-            [metabase.api.common :refer :all]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            (metabase.models [interface :as mi]
-                             [revision :as revision]
-                             [segment :refer [Segment], :as segment]
-                             [table :refer [Table]])
-            [metabase.util.schema :as su]))
+  (:require [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [interface :as mi]
+             [revision :as revision]
+             [segment :as segment :refer [Segment]]
+             [table :refer [Table]]]
+            [metabase.util.schema :as su]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
-
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Create a new `Segment`."
   [:as {{:keys [name description table_id definition]} :body}]
   {name       su/NonBlankString
    table_id   su/IntGreaterThanZero
    definition su/Map}
-  (check-superuser)
-  (write-check Table table_id)
-  (check-500 (segment/create-segment! table_id name description *current-user-id* definition)))
+  (api/check-superuser)
+  (api/write-check Table table_id)
+  (api/check-500 (segment/create-segment! table_id name description api/*current-user-id* definition)))
 
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Fetch `Segment` with ID."
   [id]
-  (check-superuser)
-  (read-check (segment/retrieve-segment id)))
+  (api/check-superuser)
+  (api/read-check (segment/retrieve-segment id)))
 
 ;; TODO - Why do we require superuser status for GET /api/segment/:id but not GET /api/segment?
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Fetch *all* `Segments`."
   []
   (filter mi/can-read? (-> (db/select Segment, :is_active true, {:order-by [[:%lower.name :asc]]})
                                (hydrate :creator))))
 
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `Segment` with ID."
-  [id :as {{:keys [name description caveats points_of_interest show_in_getting_started definition revision_message]} :body}]
+  [id :as {{:keys [name definition revision_message], :as body} :body}]
   {name             su/NonBlankString
    revision_message su/NonBlankString
    definition       su/Map}
-  (check-superuser)
-  (write-check Segment id)
+  (api/check-superuser)
+  (api/write-check Segment id)
   (segment/update-segment!
-    {:id                      id
-     :name                    name
-     :description             description
-     :caveats                 caveats
-     :points_of_interest      points_of_interest
-     :show_in_getting_started show_in_getting_started
-     :definition              definition
-     :revision_message        revision_message}
-    *current-user-id*))
+   (assoc (select-keys body #{:name :description :caveats :points_of_interest :show_in_getting_started :definition :revision_message})
+     :id id)
+   api/*current-user-id*))
 
 
-(defendpoint DELETE "/:id"
+(api/defendpoint DELETE "/:id"
   "Delete a `Segment`."
   [id revision_message]
   {revision_message su/NonBlankString}
-  (check-superuser)
-  (write-check Segment id)
-  (segment/delete-segment! id *current-user-id* revision_message)
+  (api/check-superuser)
+  (api/write-check Segment id)
+  (segment/delete-segment! id api/*current-user-id* revision_message)
   {:success true}) ; TODO - why doesn't this return a 204 'No Content'?
 
 
-(defendpoint GET "/:id/revisions"
+(api/defendpoint GET "/:id/revisions"
   "Fetch `Revisions` for `Segment` with ID."
   [id]
-  (check-superuser)
-  (read-check Segment id)
+  (api/check-superuser)
+  (api/read-check Segment id)
   (revision/revisions+details Segment id))
 
 
-(defendpoint POST "/:id/revert"
+(api/defendpoint POST "/:id/revert"
   "Revert a `Segement` to a prior `Revision`."
   [id :as {{:keys [revision_id]} :body}]
   {revision_id su/IntGreaterThanZero}
-  (check-superuser)
-  (write-check Segment id)
+  (api/check-superuser)
+  (api/write-check Segment id)
   (revision/revert!
     :entity      Segment
     :id          id
-    :user-id     *current-user-id*
+    :user-id     api/*current-user-id*
     :revision-id revision_id))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/session.clj b/src/metabase/api/session.clj
index 7dd4503679b2d4036abe220e71a15282c617477a..8432ae542678b85c31ae247352b8add6e707af41 100644
--- a/src/metabase/api/session.clj
+++ b/src/metabase/api/session.clj
@@ -1,24 +1,26 @@
 (ns metabase.api.session
   "/api/session endpoints"
-  (:require [clojure.tools.logging :as log]
-            [cemerick.friend.credentials :as creds]
+  (:require [cemerick.friend.credentials :as creds]
             [cheshire.core :as json]
             [clj-http.client :as http]
-            [compojure.core :refer [defroutes GET POST DELETE]]
+            [clojure.tools.logging :as log]
+            [compojure.core :refer [DELETE GET POST]]
+            [metabase
+             [events :as events]
+             [public-settings :as public-settings]
+             [util :as u]]
+            [metabase.api.common :as api]
+            [metabase.email.messages :as email]
+            [metabase.models
+             [session :refer [Session]]
+             [setting :refer [defsetting]]
+             [user :as user :refer [User]]]
+            [metabase.util
+             [password :as pass]
+             [schema :as su]]
             [schema.core :as s]
             [throttle.core :as throttle]
-            [toucan.db :as db]
-            [metabase.api.common :refer :all]
-            [metabase.email.messages :as email]
-            [metabase.events :as events]
-            (metabase.models [user :refer [User], :as user]
-                             [session :refer [Session]]
-                             [setting :refer [defsetting]])
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u]
-            (metabase.util [password :as pass]
-                           [schema :as su])))
-
+            [toucan.db :as db]))
 
 (defn- create-session!
   "Generate a new `Session` for a given `User`. Returns the newly generated session ID."
@@ -38,7 +40,7 @@
   {:email      (throttle/make-throttler :email)
    :ip-address (throttle/make-throttler :email, :attempts-threshold 50)}) ; IP Address doesn't have an actual UI field so just show error by email
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Login."
   [:as {{:keys [email password]} :body, remote-address :remote-addr}]
   {email    su/Email
@@ -54,13 +56,13 @@
     {:id (create-session! user)}))
 
 
-(defendpoint DELETE "/"
+(api/defendpoint DELETE "/"
   "Logout."
   [session_id]
   {session_id su/NonBlankString}
-  (check-exists? Session session_id)
+  (api/check-exists? Session session_id)
   (db/delete! Session :id session_id)
-  generic-204-no-content)
+  api/generic-204-no-content)
 
 ;; Reset tokens:
 ;; We need some way to match a plaintext token with the a user since the token stored in the DB is hashed.
@@ -73,7 +75,7 @@
   {:email      (throttle/make-throttler :email)
    :ip-address (throttle/make-throttler :email, :attempts-threshold 50)})
 
-(defendpoint POST "/forgot_password"
+(api/defendpoint POST "/forgot_password"
   "Send a reset email when user has forgotten their password."
   [:as {:keys [server-name] {:keys [email]} :body, remote-address :remote-addr}]
   {email su/Email}
@@ -105,7 +107,7 @@
             (when (< token-age reset-token-ttl-ms)
               user)))))))
 
-(defendpoint POST "/reset_password"
+(api/defendpoint POST "/reset_password"
   "Reset password with a reset token."
   [:as {{:keys [token password]} :body}]
   {token    su/NonBlankString
@@ -119,17 +121,17 @@
         ;; after a successful password update go ahead and offer the client a new session that they can use
         {:success    true
          :session_id (create-session! user)})
-      (throw-invalid-param-exception :password "Invalid reset token")))
+      (api/throw-invalid-param-exception :password "Invalid reset token")))
 
 
-(defendpoint GET "/password_reset_token_valid"
+(api/defendpoint GET "/password_reset_token_valid"
   "Check is a password reset token is valid and isn't expired."
   [token]
   {token s/Str}
   {:valid (boolean (valid-reset-token->user token))})
 
 
-(defendpoint GET "/properties"
+(api/defendpoint GET "/properties"
   "Get all global properties and their values. These are the specific `Settings` which are meant to be public."
   []
   (public-settings/public-settings))
@@ -183,7 +185,7 @@
                     (google-auth-create-new-user! first-name last-name email))]
     {:id (create-session! user)}))
 
-(defendpoint POST "/google_auth"
+(api/defendpoint POST "/google_auth"
   "Login with Google Auth."
   [:as {{:keys [token]} :body, remote-address :remote-addr}]
   {token su/NonBlankString}
@@ -194,4 +196,4 @@
     (google-auth-fetch-or-create-user! given_name family_name email)))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/setting.clj b/src/metabase/api/setting.clj
index a0c75c6edb6509cb4e9d71e72a66048bee2263c8..b1b943c545339e6e817630d634c0dbc4cd3ba5c8 100644
--- a/src/metabase/api/setting.clj
+++ b/src/metabase/api/setting.clj
@@ -1,34 +1,34 @@
 (ns metabase.api.setting
   "/api/setting endpoints"
-  (:require [compojure.core :refer [GET PUT DELETE]]
-            [metabase.api.common :refer :all]
+  (:require [compojure.core :refer [GET PUT]]
+            [metabase.api.common :as api]
             [metabase.models.setting :as setting]
             [metabase.util.schema :as su]))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Get all `Settings` and their values. You must be a superuser to do this."
   []
-  (check-superuser)
+  (api/check-superuser)
   (setting/all))
 
-(defendpoint GET "/:key"
+(api/defendpoint GET "/:key"
   "Fetch a single `Setting`. You must be a superuser to do this."
   [key]
   {key su/NonBlankString}
-  (check-superuser)
+  (api/check-superuser)
   (let [k (keyword key)
         v (setting/get k)]
     ;; for security purposes, don't return value of a setting if it was defined via env var
     (when (not= v (setting/env-var-value k))
       v)))
 
-(defendpoint PUT "/:key"
+(api/defendpoint PUT "/:key"
   "Create/update a `Setting`. You must be a superuser to do this.
    This endpoint can also be used to delete Settings by passing `nil` for `:value`."
   [key :as {{:keys [value]} :body}]
   {key su/NonBlankString}
-  (check-superuser)
+  (api/check-superuser)
   (setting/set! key value))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/setup.clj b/src/metabase/api/setup.clj
index fe6ae3b9a91099e4bd7bba3e90e24fd0588a6b94..078f7a1615f7347d47f229bf80143bf558b7f27e 100644
--- a/src/metabase/api/setup.clj
+++ b/src/metabase/api/setup.clj
@@ -1,22 +1,24 @@
 (ns metabase.api.setup
   (:require [compojure.core :refer [GET POST]]
             [medley.core :as m]
-            [schema.core :as s]
-            [toucan.db :as db]
-            (metabase.api [common :refer :all]
-                          [database :refer [DBEngine]])
-            (metabase [driver :as driver]
-                      [email :as email]
-                      [events :as events])
+            [metabase
+             [driver :as driver]
+             [email :as email]
+             [events :as events]
+             [public-settings :as public-settings]
+             [setup :as setup]
+             [util :as u]]
+            [metabase.api
+             [common :as api]
+             [database :refer [DBEngine]]]
             [metabase.integrations.slack :as slack]
-            (metabase.models [database :refer [Database]]
-                             [session :refer [Session]]
-                             [setting :as setting]
-                             [user :refer [User], :as user])
-            [metabase.public-settings :as public-settings]
-            [metabase.setup :as setup]
-            [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.models
+             [database :refer [Database]]
+             [session :refer [Session]]
+             [user :as user :refer [User]]]
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 (def ^:private SetupToken
   "Schema for a string that matches the instance setup token."
@@ -24,16 +26,17 @@
     "Token does not match the setup token."))
 
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Special endpoint for creating the first user during setup.
    This endpoint both creates the user AND logs them in and returns a session ID."
   [:as {{:keys [token] {:keys [name engine details is_full_sync]} :database, {:keys [first_name last_name email password]} :user, {:keys [allow_tracking site_name]} :prefs} :body}]
-  {token      SetupToken
-   site_name  su/NonBlankString
-   first_name su/NonBlankString
-   last_name  su/NonBlankString
-   email      su/Email
-   password   su/ComplexPassword}
+  {token          SetupToken
+   site_name      su/NonBlankString
+   first_name     su/NonBlankString
+   last_name      su/NonBlankString
+   email          su/Email
+   password       su/ComplexPassword
+   allow_tracking (s/maybe (s/cond-pre s/Bool su/BooleanString))}
   ;; Now create the user
   (let [session-id (str (java.util.UUID/randomUUID))
         new-user   (db/insert! User
@@ -47,18 +50,16 @@
     ;; set a couple preferences
     (public-settings/site-name site_name)
     (public-settings/admin-email email)
-    (public-settings/anon-tracking-enabled (if (m/boolean? allow_tracking)
-                                             allow_tracking
-                                             true)) ; default to `true` if allow_tracking isn't specified
+    (public-settings/anon-tracking-enabled (or (nil? allow_tracking) ; default to `true` if allow_tracking isn't specified
+                                               allow_tracking))      ; the setting will set itself correctly whether a boolean or boolean string is specified
     ;; setup database (if needed)
     (when (driver/is-engine? engine)
       (->> (db/insert! Database
              :name         name
              :engine       engine
              :details      details
-             :is_full_sync (if-not (nil? is_full_sync)
-                             is_full_sync
-                             true))
+             :is_full_sync (or (nil? is_full_sync) ; default to `true` is `is_full_sync` isn't specified
+                               is_full_sync))
            (events/publish-event! :database-create)))
     ;; clear the setup token now, it's no longer needed
     (setup/clear-token!)
@@ -72,7 +73,7 @@
     {:id session-id}))
 
 
-(defendpoint POST "/validate"
+(api/defendpoint POST "/validate"
   "Validate that we can connect to a database given a set of details."
   [:as {{{:keys [engine] {:keys [host port] :as details} :details} :details, token :token} :body}]
   {token  SetupToken
@@ -183,11 +184,11 @@
 (defn- admin-checklist []
   (partition-steps-into-groups (add-next-step-info (admin-checklist-values))))
 
-(defendpoint GET "/admin_checklist"
+(api/defendpoint GET "/admin_checklist"
   "Return various \"admin checklist\" steps and whether they've been completed. You must be a superuser to see this!"
   []
-  (check-superuser)
+  (api/check-superuser)
   (admin-checklist))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/slack.clj b/src/metabase/api/slack.clj
index c0ba6cd107bf4d8010f08d2ce868fcc53adca913..1521a2d8777dd00cdefb4f6a1eacdafe8e0554d6 100644
--- a/src/metabase/api/slack.clj
+++ b/src/metabase/api/slack.clj
@@ -1,19 +1,19 @@
 (ns metabase.api.slack
   "/api/slack endpoints"
   (:require [compojure.core :refer [PUT]]
-            [schema.core :as s]
-            [metabase.api.common :refer :all]
+            [metabase.api.common :as api]
             [metabase.config :as config]
             [metabase.integrations.slack :as slack]
             [metabase.models.setting :as setting]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]))
 
-(defendpoint PUT "/settings"
+(api/defendpoint PUT "/settings"
   "Update Slack related settings. You must be a superuser to do this."
   [:as {{slack-token :slack-token, metabot-enabled :metabot-enabled, :as slack-settings} :body}]
   {slack-token     (s/maybe su/NonBlankString)
    metabot-enabled s/Bool}
-  (check-superuser)
+  (api/check-superuser)
   (if-not slack-token
     (setting/set-many! {:slack-token nil, :metabot-enabled false})
     (try
@@ -25,4 +25,4 @@
       (catch clojure.lang.ExceptionInfo info
         {:status 400, :body (ex-data info)}))))
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/table.clj b/src/metabase/api/table.clj
index ea06b1ef9aa25b5bd25049977c9cd229ed760366..4c589b8654fa86a7aeb064a9eaacc2c3f5e9f56e 100644
--- a/src/metabase/api/table.clj
+++ b/src/metabase/api/table.clj
@@ -1,16 +1,16 @@
 (ns metabase.api.table
   "/api/table endpoints."
-  (:require [compojure.core :refer [GET POST PUT]]
+  (:require [compojure.core :refer [GET PUT]]
+            [metabase.api.common :as api]
+            [metabase.models
+             [field :refer [Field]]
+             [interface :as mi]
+             [table :as table :refer [Table]]]
+            [metabase.util.schema :as su]
             [schema.core :as s]
-            [metabase.api.common :refer :all]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]])
-            [metabase.driver :as driver]
-            (metabase.models [field :refer [Field]]
-                             [interface :as mi]
-                             [table :refer [Table] :as table])
-            [metabase.sync-database :as sync-database]
-            [metabase.util.schema :as su]))
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]]))
 
 ;; TODO - I don't think this is used for anything any more
 (def ^:private ^:deprecated TableEntityType
@@ -21,7 +21,7 @@
   "Schema for a valid table visibility type."
   (apply s/enum (map name table/visibility-types)))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Get all `Tables`."
   []
   (for [table (-> (db/select Table, :active true, {:order-by [[:name :asc]]})
@@ -31,31 +31,31 @@
     (update table :rows (fn [n]
                           (or n 0)))))
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Get `Table` with ID."
   [id]
-  (-> (read-check Table id)
+  (-> (api/read-check Table id)
       (hydrate :db :pk_field)))
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update `Table` with ID."
   [id :as {{:keys [display_name entity_type visibility_type description caveats points_of_interest show_in_getting_started]} :body}]
   {display_name    (s/maybe su/NonBlankString)
    entity_type     (s/maybe TableEntityType)
    visibility_type (s/maybe TableVisibilityType)}
-  (write-check Table id)
-  (check-500 (db/update-non-nil-keys! Table id
-               :display_name            display_name
-               :caveats                 caveats
-               :points_of_interest      points_of_interest
-               :show_in_getting_started show_in_getting_started
-               :entity_type             entity_type
-               :description             description))
-  (check-500 (db/update! Table id, :visibility_type visibility_type))
+  (api/write-check Table id)
+  (api/check-500 (db/update-non-nil-keys! Table id
+                   :display_name            display_name
+                   :caveats                 caveats
+                   :points_of_interest      points_of_interest
+                   :show_in_getting_started show_in_getting_started
+                   :entity_type             entity_type
+                   :description             description))
+  (api/check-500 (db/update! Table id, :visibility_type visibility_type))
   (Table id))
 
 
-(defendpoint GET "/:id/query_metadata"
+(api/defendpoint GET "/:id/query_metadata"
   "Get metadata about a `Table` useful for running queries.
    Returns DB, fields, field FKs, and field values.
 
@@ -63,7 +63,7 @@
   will any of its corresponding values be returned. (This option is provided for use in the Admin Edit Metadata page)."
   [id include_sensitive_fields]
   {include_sensitive_fields (s/maybe su/BooleanString)}
-  (-> (read-check Table id)
+  (-> (api/read-check Table id)
       (hydrate :db [:fields :target] :field_values :segments :metrics)
       (update-in [:fields] (if (Boolean/parseBoolean include_sensitive_fields)
                              ;; If someone passes include_sensitive_fields return hydrated :fields as-is
@@ -73,10 +73,10 @@
                                                (not= (keyword visibility_type) :sensitive)))))))
 
 
-(defendpoint GET "/:id/fks"
+(api/defendpoint GET "/:id/fks"
   "Get all foreign keys whose destination is a `Field` that belongs to this `Table`."
   [id]
-  (read-check Table id)
+  (api/read-check Table id)
   (when-let [field-ids (seq (db/select-ids Field, :table_id id, :visibility_type [:not= "retired"], :active true))]
     (for [origin-field (db/select Field, :fk_target_field_id [:in field-ids], :active true)]
       ;; it's silly to be hydrating some of these tables/dbs
@@ -87,4 +87,4 @@
        :destination    (hydrate (Field (:fk_target_field_id origin-field)) :table)})))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/tiles.clj b/src/metabase/api/tiles.clj
index 430330ebef951dad9ad29ddd4711a323118b5802..4ebc3f16da261d37be303b278b3f254689ffdeea 100644
--- a/src/metabase/api/tiles.clj
+++ b/src/metabase/api/tiles.clj
@@ -1,16 +1,16 @@
 (ns metabase.api.tiles
   "`/api/tiles` endpoints."
-  (:require [clojure.core.match :refer [match]]
-            [clojure.java.io :as io]
-            [cheshire.core :as json]
+  (:require [cheshire.core :as json]
+            [clojure.core.match :refer [match]]
             [compojure.core :refer [GET]]
-            [metabase.api.common :refer :all]
-            [metabase.query-processor :as qp]
-            [metabase.util :as u]
+            [metabase
+             [query-processor :as qp]
+             [util :as u]]
+            [metabase.api.common :as api]
             [metabase.util.schema :as su])
   (:import java.awt.Color
            java.awt.image.BufferedImage
-           (java.io ByteArrayOutputStream IOException)
+           java.io.ByteArrayOutputStream
            javax.imageio.ImageIO))
 
 ;;; # ------------------------------------------------------------ CONSTANTS ------------------------------------------------------------
@@ -109,7 +109,7 @@
 
 ;;; # ------------------------------------------------------------ ENDPOINT ------------------------------------------------------------
 
-(defendpoint GET "/:zoom/:x/:y/:lat-field-id/:lon-field-id/:lat-col-idx/:lon-col-idx/"
+(api/defendpoint GET "/:zoom/:x/:y/:lat-field-id/:lon-field-id/:lat-col-idx/:lon-col-idx/"
   "This endpoints provides an image with the appropriate pins rendered given a MBQL QUERY (passed as a GET query string param).
    We evaluate the query and find the set of lat/lon pairs which are relevant and then render the appropriate ones.
    It's expected that to render a full map view several calls will be made to this endpoint in parallel."
@@ -129,7 +129,7 @@
         lon-col-idx   (Integer/parseInt lon-col-idx)
         query         (json/parse-string query keyword)
         updated-query (update query :query (u/rpartial query-with-inside-filter lat-field-id lon-field-id x y zoom))
-        result        (qp/dataset-query updated-query {:executed-by *current-user-id*, :context :map-tiles})
+        result        (qp/dataset-query updated-query {:executed-by api/*current-user-id*, :context :map-tiles})
         points        (for [row (-> result :data :rows)]
                         [(nth row lat-col-idx) (nth row lon-col-idx)])]
     ;; manual ring response here.  we simply create an inputstream from the byte[] of our image
@@ -138,4 +138,4 @@
      :body    (java.io.ByteArrayInputStream. (tile->byte-array (create-tile zoom points)))}))
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/user.clj b/src/metabase/api/user.clj
index 173de93aebe262bcae14163ecd720f208f7480c9..8358b439365942f93d99e34d8119fa637e6e3672 100644
--- a/src/metabase/api/user.clj
+++ b/src/metabase/api/user.clj
@@ -1,23 +1,24 @@
 (ns metabase.api.user
   (:require [cemerick.friend.credentials :as creds]
-            [compojure.core :refer [defroutes GET DELETE POST PUT]]
-            [schema.core :as s]
-            [metabase.api.common :refer :all]
-            [metabase.api.session :as session-api]
-            [toucan.db :as db]
+            [compojure.core :refer [DELETE GET POST PUT]]
+            [metabase.api
+             [common :as api]
+             [session :as session-api]]
             [metabase.email.messages :as email]
-            [metabase.models.user :as user, :refer [User]]
+            [metabase.models.user :as user :refer [User]]
             [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 (defn- check-self-or-superuser
-  "Check that USER-ID is `*current-user-id*` or that `*current-user*` is a superuser, or throw a 403."
+  "Check that USER-ID is *current-user-id*` or that `*current-user*` is a superuser, or throw a 403."
   [user-id]
   {:pre [(integer? user-id)]}
-  (check-403 (or (= user-id *current-user-id*)
-                 (:is_superuser @*current-user*))))
+  (api/check-403 (or (= user-id api/*current-user-id*)
+                     (:is_superuser @api/*current-user*))))
 
-(defendpoint GET "/"
+(api/defendpoint GET "/"
   "Fetch a list of all active `Users` for the admin People page."
   []
   (db/select [User :id :first_name :last_name :email :is_superuser :google_auth :last_login], :is_active true))
@@ -36,92 +37,92 @@
   (User (u/get-id existing-user)))
 
 
-(defendpoint POST "/"
+(api/defendpoint POST "/"
   "Create a new `User`, or or reäctivate an existing one."
   [:as {{:keys [first_name last_name email password]} :body}]
   {first_name su/NonBlankString
    last_name  su/NonBlankString
    email      su/Email}
-  (check-superuser)
+  (api/check-superuser)
   (if-let [existing-user (db/select-one [User :id :is_active :google_auth], :email email)]
     ;; this user already exists but is inactive, so simply reactivate the account
     (reactivate-user! existing-user first_name last_name)
     ;; new user account, so create it
-    (user/invite-user! first_name last_name email password @*current-user*)))
+    (user/invite-user! first_name last_name email password @api/*current-user*)))
 
 
-(defendpoint GET "/current"
+(api/defendpoint GET "/current"
   "Fetch the current `User`."
   []
-  (check-404 @*current-user*))
+  (api/check-404 @api/*current-user*))
 
 
-(defendpoint GET "/:id"
+(api/defendpoint GET "/:id"
   "Fetch a `User`. You must be fetching yourself *or* be a superuser."
   [id]
   (check-self-or-superuser id)
-  (check-404 (User :id id, :is_active true)))
+  (api/check-404 (User :id id, :is_active true)))
 
 
-(defendpoint PUT "/:id"
+(api/defendpoint PUT "/:id"
   "Update a `User`."
   [id :as {{:keys [email first_name last_name is_superuser]} :body}]
   {email      su/Email
    first_name (s/maybe su/NonBlankString)
    last_name  (s/maybe su/NonBlankString)}
   (check-self-or-superuser id)
-  (check-404 (db/exists? User, :id id, :is_active true))            ; only allow updates if the specified account is active
-  (check-400 (not (db/exists? User, :email email, :id [:not= id]))) ; can't change email if it's already taken BY ANOTHER ACCOUNT
-  (check-500 (db/update-non-nil-keys! User id
-               :email        email
-               :first_name   first_name
-               :last_name    last_name
-               :is_superuser (when (:is_superuser @*current-user*)
-                               is_superuser)))
+  (api/check-404 (db/exists? User, :id id, :is_active true)) ; only allow updates if the specified account is active
+  (api/check-400 (not (db/exists? User, :email email, :id [:not= id]))) ; can't change email if it's already taken BY ANOTHER ACCOUNT
+  (api/check-500 (db/update-non-nil-keys! User id
+                   :email        email
+                   :first_name   first_name
+                   :last_name    last_name
+                   :is_superuser (when (:is_superuser @api/*current-user*)
+                                   is_superuser)))
   (User id))
 
 
-(defendpoint PUT "/:id/password"
+(api/defendpoint PUT "/:id/password"
   "Update a user's password."
   [id :as {{:keys [password old_password]} :body}]
   {password su/ComplexPassword}
   (check-self-or-superuser id)
-  (let-404 [user (db/select-one [User :password_salt :password], :id id, :is_active true)]
+  (api/let-404 [user (db/select-one [User :password_salt :password], :id id, :is_active true)]
     ;; admins are allowed to reset anyone's password (in the admin people list) so no need to check the value of `old_password` for them
     ;; regular users have to know their password, however
-    (when-not (:is_superuser @*current-user*)
-      (checkp (creds/bcrypt-verify (str (:password_salt user) old_password) (:password user)) "old_password" "Invalid password")))
+    (when-not (:is_superuser @api/*current-user*)
+      (api/checkp (creds/bcrypt-verify (str (:password_salt user) old_password) (:password user)) "old_password" "Invalid password")))
   (user/set-password! id password)
   ;; return the updated User
   (User id))
 
 
 ;; TODO - This could be handled by PUT /api/user/:id, we don't need a separate endpoint
-(defendpoint PUT "/:id/qbnewb"
+(api/defendpoint PUT "/:id/qbnewb"
   "Indicate that a user has been informed about the vast intricacies of 'the' Query Builder."
   [id]
   (check-self-or-superuser id)
-  (check-500 (db/update! User id, :is_qbnewb false))
+  (api/check-500 (db/update! User id, :is_qbnewb false))
   {:success true})
 
 
-(defendpoint POST "/:id/send_invite"
+(api/defendpoint POST "/:id/send_invite"
   "Resend the user invite email for a given user."
   [id]
-  (check-superuser)
+  (api/check-superuser)
   (when-let [user (User :id id, :is_active true)]
     (let [reset-token (user/set-password-reset-token! id)
           ;; NOTE: the new user join url is just a password reset with an indicator that this is a first time user
           join-url    (str (user/form-password-reset-url reset-token) "#new")]
-      (email/send-new-user-email! user @*current-user* join-url))))
+      (email/send-new-user-email! user @api/*current-user* join-url))))
 
 
-(defendpoint DELETE "/:id"
+(api/defendpoint DELETE "/:id"
   "Disable a `User`.  This does not remove the `User` from the DB, but instead disables their account."
   [id]
-  (check-superuser)
-  (check-500 (db/update! User id, :is_active false))
+  (api/check-superuser)
+  (api/check-500 (db/update! User id, :is_active false))
   {:success true})
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/api/util.clj b/src/metabase/api/util.clj
index 0bf9fb70b9c00825f438424c9fe3cef429592636..6791ff30be33f1700238cf376ecab94c28340b7d 100644
--- a/src/metabase/api/util.clj
+++ b/src/metabase/api/util.clj
@@ -1,36 +1,37 @@
 (ns metabase.api.util
   "Random utilty endpoints for things that don't belong anywhere else in particular, e.g. endpoints for certain admin page tasks."
-  (:require [compojure.core :refer [defroutes GET POST]]
+  (:require [compojure.core :refer [GET POST]]
             [crypto.random :as crypto-random]
-            [metabase.api.common :refer :all]
+            [metabase.api.common :as api]
             [metabase.logger :as logger]
-            [metabase.util.schema :as su]
-            [metabase.util.stats :as stats]))
+            [metabase.util
+             [schema :as su]
+             [stats :as stats]]))
 
-(defendpoint POST "/password_check"
+(api/defendpoint POST "/password_check"
   "Endpoint that checks if the supplied password meets the currently configured password complexity rules."
   [:as {{:keys [password]} :body}]
   {password su/ComplexPassword} ;; if we pass the su/ComplexPassword test we're g2g
   {:valid true})
 
-(defendpoint GET "/logs"
+(api/defendpoint GET "/logs"
   "Logs."
   []
-  (check-superuser)
+  (api/check-superuser)
   (logger/get-messages))
 
-(defendpoint GET "/stats"
+(api/defendpoint GET "/stats"
   "Anonymous usage stats. Endpoint for testing, and eventually exposing this to instance admins to let them see
   what is being phoned home."
   []
-  (check-superuser)
+  (api/check-superuser)
   (stats/anonymous-usage-stats))
 
-(defendpoint GET "/random_token"
+(api/defendpoint GET "/random_token"
   "Return a cryptographically secure random 32-byte token, encoded as a hexidecimal string.
    Intended for use when creating a value for `embedding-secret-key`."
   []
   {:token (crypto-random/hex 32)})
 
 
-(define-routes)
+(api/define-routes)
diff --git a/src/metabase/cmd/endpoint_dox.clj b/src/metabase/cmd/endpoint_dox.clj
index afccd42eb996f404daf509623fa93957528e8419..075eef56ad9f42e583b44572e2fdbee0e2d1084d 100644
--- a/src/metabase/cmd/endpoint_dox.clj
+++ b/src/metabase/cmd/endpoint_dox.clj
@@ -2,9 +2,9 @@
   "Implementation for the `api-documentation` command, which generate"
   (:require [clojure.java.io :as io]
             [clojure.string :as str]
-            [metabase.config :as config]
-            [metabase.util :as u]))
-
+            [metabase
+             [config :as config]
+             [util :as u]]))
 
 (defn- dox
   "Generate a Markdown string containing documentation for all Metabase API endpoints."
diff --git a/src/metabase/cmd/load_from_h2.clj b/src/metabase/cmd/load_from_h2.clj
index 879b4e35ac043a33068fcadc2387b8b9c38370ec..3fa41d4596abf8c1bcee0918eb80a5fd1d7e2247 100644
--- a/src/metabase/cmd/load_from_h2.clj
+++ b/src/metabase/cmd/load_from_h2.clj
@@ -15,49 +15,48 @@
    MB_DB_TYPE=mysql MB_DB_HOST=localhost MB_DB_PORT=3305 MB_DB_USER=root MB_DB_DBNAME=metabase lein run load-from-h2
    ```"
   (:require [clojure.java.jdbc :as jdbc]
-            [clojure.set :as set]
             [colorize.core :as color]
             [medley.core :as m]
-            [metabase.config :as config]
-            [toucan.db :as db]
-            [metabase.db :as mdb]
+            [metabase
+             [config :as config]
+             [db :as mdb]
+             [util :as u]]
             [metabase.db.migrations :refer [DataMigrations]]
-            (metabase.models [activity :refer [Activity]]
-                             [card :refer [Card]]
-                             [card-favorite :refer [CardFavorite]]
-                             [card-label :refer [CardLabel]]
-                             [collection :refer [Collection]]
-                             [collection-revision :refer [CollectionRevision]]
-                             [dashboard :refer [Dashboard]]
-                             [dashboard-card :refer [DashboardCard]]
-                             [dashboard-card-series :refer [DashboardCardSeries]]
-                             [database :refer [Database]]
-                             [dependency :refer [Dependency]]
-                             [field :refer [Field]]
-                             [field-values :refer [FieldValues]]
-                             [label :refer [Label]]
-                             [metric :refer [Metric]]
-                             [metric-important-field :refer [MetricImportantField]]
-                             [permissions :refer [Permissions]]
-                             [permissions-group :refer [PermissionsGroup]]
-                             [permissions-group-membership :refer [PermissionsGroupMembership]]
-                             [permissions-revision :refer [PermissionsRevision]]
-                             [pulse :refer [Pulse]]
-                             [pulse-card :refer [PulseCard]]
-                             [pulse-channel :refer [PulseChannel]]
-                             [pulse-channel-recipient :refer [PulseChannelRecipient]]
-                             [raw-column :refer [RawColumn]]
-                             [raw-table :refer [RawTable]]
-                             [revision :refer [Revision]]
-                             [segment :refer [Segment]]
-                             [session :refer [Session]]
-                             [setting :refer [Setting]]
-                             [table :refer [Table]]
-                             [user :refer [User]]
-                             [view-log :refer [ViewLog]])
-            [metabase.util :as u]
-            [clojure.tools.logging :as log]))
-
+            [metabase.models
+             [activity :refer [Activity]]
+             [card :refer [Card]]
+             [card-favorite :refer [CardFavorite]]
+             [card-label :refer [CardLabel]]
+             [collection :refer [Collection]]
+             [collection-revision :refer [CollectionRevision]]
+             [dashboard :refer [Dashboard]]
+             [dashboard-card :refer [DashboardCard]]
+             [dashboard-card-series :refer [DashboardCardSeries]]
+             [dashboard-favorite :refer [DashboardFavorite]]
+             [database :refer [Database]]
+             [dependency :refer [Dependency]]
+             [field :refer [Field]]
+             [field-values :refer [FieldValues]]
+             [label :refer [Label]]
+             [metric :refer [Metric]]
+             [metric-important-field :refer [MetricImportantField]]
+             [permissions :refer [Permissions]]
+             [permissions-group :refer [PermissionsGroup]]
+             [permissions-group-membership :refer [PermissionsGroupMembership]]
+             [permissions-revision :refer [PermissionsRevision]]
+             [pulse :refer [Pulse]]
+             [pulse-card :refer [PulseCard]]
+             [pulse-channel :refer [PulseChannel]]
+             [pulse-channel-recipient :refer [PulseChannelRecipient]]
+             [raw-column :refer [RawColumn]]
+             [raw-table :refer [RawTable]]
+             [revision :refer [Revision]]
+             [segment :refer [Segment]]
+             [session :refer [Session]]
+             [setting :refer [Setting]]
+             [table :refer [Table]]
+             [user :refer [User]]
+             [view-log :refer [ViewLog]]]))
 
 (defn- println-ok [] (println (color/green "[OK]")))
 
@@ -100,6 +99,7 @@
    PermissionsRevision
    Collection
    CollectionRevision
+   DashboardFavorite
    ;; migrate the list of finished DataMigrations as the very last thing (all models to copy over should be listed above this line)
    DataMigrations])
 
diff --git a/src/metabase/core.clj b/src/metabase/core.clj
index 571ae867587aa8bcbe03f53e6a779b01e91091f1..c36cfe2750630b5b58c2ce441bb1605d12bcbe9e 100644
--- a/src/metabase/core.clj
+++ b/src/metabase/core.clj
@@ -1,35 +1,36 @@
 ;; -*- comment-column: 35; -*-
 (ns metabase.core
   (:gen-class)
-  (:require (clojure [pprint :as pprint]
-                     [string :as s])
+  (:require [clojure
+             [pprint :as pprint]
+             [string :as s]]
             [clojure.tools.logging :as log]
             environ.core
-            [ring.adapter.jetty :as ring-jetty]
-            (ring.middleware [cookies :refer [wrap-cookies]]
-                             [gzip :refer [wrap-gzip]]
-                             [json :refer [wrap-json-response
-                                           wrap-json-body]]
-                             [keyword-params :refer [wrap-keyword-params]]
-                             [params :refer [wrap-params]]
-                             [session :refer [wrap-session]])
             [medley.core :as m]
-            [toucan.db :as db]
-            [metabase.config :as config]
+            [metabase
+             [config :as config]
+             [db :as mdb]
+             [driver :as driver]
+             [events :as events]
+             [metabot :as metabot]
+             [middleware :as mb-middleware]
+             [plugins :as plugins]
+             [routes :as routes]
+             [sample-data :as sample-data]
+             [setup :as setup]
+             [task :as task]
+             [util :as u]]
             [metabase.core.initialization-status :as init-status]
-            (metabase [db :as mdb]
-                      [driver :as driver]
-                      [events :as events]
-                      [logger :as logger]
-                      [metabot :as metabot]
-                      [middleware :as mb-middleware]
-                      [plugins :as plugins]
-                      [routes :as routes]
-                      [sample-data :as sample-data]
-                      [setup :as setup]
-                      [task :as task]
-                      [util :as u])
-            [metabase.models.user :refer [User]])
+            [metabase.models.user :refer [User]]
+            [ring.adapter.jetty :as ring-jetty]
+            [ring.middleware
+             [cookies :refer [wrap-cookies]]
+             [gzip :refer [wrap-gzip]]
+             [json :refer [wrap-json-body wrap-json-response]]
+             [keyword-params :refer [wrap-keyword-params]]
+             [params :refer [wrap-params]]
+             [session :refer [wrap-session]]]
+            [toucan.db :as db])
   (:import org.eclipse.jetty.server.Server))
 
 ;;; CONFIG
diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index bf20b1a09ff0bc758ce5d17c64fcfa84764a9211..e80d562feccc63aafade673d9c164adc1e212c8a 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -1,32 +1,26 @@
 (ns metabase.db
   "Database definition and helper functions for interacting with the database."
-  (:require (clojure.java [io :as io]
-                          [jdbc :as jdbc])
+  (:require [clojure
+             [string :as s]
+             [walk :as walk]]
+            [clojure.java
+             [io :as io]
+             [jdbc :as jdbc]]
             [clojure.tools.logging :as log]
-            (clojure [set :as set]
-                     [string :as s]
-                     [walk :as walk])
-            (honeysql [core :as hsql]
-                      [format :as hformat]
-                      [helpers :as h])
-            [medley.core :as m]
-            [ring.util.codec :as codec]
-            [toucan.db :as db]
-            [metabase.config :as config]
+            [metabase
+             [config :as config]
+             [util :as u]]
             [metabase.db.spec :as dbspec]
-            [metabase.models.interface :as models]
-            [metabase.util :as u]
-            metabase.util.honeysql-extensions) ; this needs to be loaded so the `:h2` quoting style gets added
-  (:import java.io.StringWriter
-           java.sql.Connection
+            [ring.util.codec :as codec]
+            [toucan.db :as db])
+  (:import com.mchange.v2.c3p0.ComboPooledDataSource
+           java.io.StringWriter
            java.util.Properties
-           com.mchange.v2.c3p0.ComboPooledDataSource
-           liquibase.Liquibase
-           (liquibase.database DatabaseFactory Database)
+           [liquibase.database Database DatabaseFactory]
            liquibase.database.jvm.JdbcConnection
+           liquibase.Liquibase
            liquibase.resource.ClassLoaderResourceAccessor))
 
-
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 ;;; |                                              DB FILE & CONNECTION DETAILS                                              |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
diff --git a/src/metabase/db/metadata_queries.clj b/src/metabase/db/metadata_queries.clj
index 516f9f0a1c5e22e1c9da9019b188cb0722d346df..1d0ff0b45297c6c5dfdabaf1491d854cfa65fb7c 100644
--- a/src/metabase/db/metadata_queries.clj
+++ b/src/metabase/db/metadata_queries.clj
@@ -1,10 +1,11 @@
 (ns metabase.db.metadata-queries
   "Predefined MBQL queries for getting metadata about an external database."
-  (:require [toucan.db :as db]
+  (:require [metabase
+             [query-processor :as qp]
+             [util :as u]]
             [metabase.models.table :refer [Table]]
-            [metabase.query-processor :as qp]
             [metabase.query-processor.expand :as ql]
-            [metabase.util :as u]))
+            [toucan.db :as db]))
 
 (defn- qp-query [db-id query]
   (-> (qp/process-query
diff --git a/src/metabase/db/migrations.clj b/src/metabase/db/migrations.clj
index 5651790cc8437a2c4a3c96c0240413c9276d7e0d..716c65bdd6c556f9eb98424b4730798adafd6cd6 100644
--- a/src/metabase/db/migrations.clj
+++ b/src/metabase/db/migrations.clj
@@ -9,33 +9,31 @@
   (:require [clojure.java.jdbc :as jdbc]
             [clojure.string :as str]
             [clojure.tools.logging :as log]
-            [schema.core :as s]
-            (toucan [db :as db]
-                    [models :as models])
-            (metabase [config :as config]
-                      [driver :as driver]
-                      types)
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.events.activity-feed :refer [activity-feed-topics]]
-            (metabase.models [activity :refer [Activity]]
-                             [card :refer [Card]]
-                             [card-label :refer [CardLabel]]
-                             [collection :refer [Collection], :as collection]
-                             [dashboard-card :refer [DashboardCard]]
-                             [database :refer [Database]]
-                             [field :refer [Field]]
-                             [label :refer [Label]]
-                             [permissions :refer [Permissions], :as perms]
-                             [permissions-group :as perm-group]
-                             [permissions-group-membership :refer [PermissionsGroupMembership], :as perm-membership]
-                             [query-execution :refer [QueryExecution], :as query-execution]
-                             [raw-column :refer [RawColumn]]
-                             [raw-table :refer [RawTable]]
-                             [table :refer [Table] :as table]
-                             [setting :refer [Setting], :as setting]
-                             [user :refer [User]])
-            [metabase.public-settings :as public-settings]
+            [metabase.models
+             [activity :refer [Activity]]
+             [card :refer [Card]]
+             [dashboard-card :refer [DashboardCard]]
+             [database :refer [Database]]
+             [field :refer [Field]]
+             [permissions :as perms :refer [Permissions]]
+             [permissions-group :as perm-group]
+             [permissions-group-membership :as perm-membership :refer [PermissionsGroupMembership]]
+             [query-execution :as query-execution :refer [QueryExecution]]
+             [raw-column :refer [RawColumn]]
+             [raw-table :refer [RawTable]]
+             [setting :as setting :refer [Setting]]
+             [table :as table :refer [Table]]
+             [user :refer [User]]]
             [metabase.query-processor.util :as qputil]
-            [metabase.util :as u]))
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;;; # Migration Helpers
 
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index a39d9bbbe96047443d5da525c6c7c2f452ed19d6..cb9ac18032bdc9cc1662e9987afc9eea63d3323d 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -2,17 +2,17 @@
   (:require [clojure.math.numeric-tower :as math]
             [clojure.tools.logging :as log]
             [medley.core :as m]
-            [toucan.db :as db]
-            [metabase.config :as config]
-            (metabase.models [database :refer [Database]]
-                             field
-                             [query-execution :refer [QueryExecution]]
-                             [setting :refer [defsetting]])
-            [metabase.util :as u])
+            [metabase.models
+             [database :refer [Database]]
+             field
+             [setting :refer [defsetting]]
+             table]
+            [metabase.util :as u]
+            [toucan.db :as db])
   (:import clojure.lang.Keyword
            metabase.models.database.DatabaseInstance
-           metabase.models.field.FieldInstance))
-
+           metabase.models.field.FieldInstance
+           metabase.models.table.TableInstance))
 
 ;;; ## INTERFACE + CONSTANTS
 
diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj
index 6e9b011b647f2770f88ba82ed3b44dbb639201bb..3433b30a4fb797640912b08b6142a1b77c08da1c 100644
--- a/src/metabase/driver/bigquery.clj
+++ b/src/metabase/driver/bigquery.clj
@@ -1,37 +1,34 @@
 (ns metabase.driver.bigquery
-  (:require (clojure [set :as set]
-                     [string :as s]
-                     [walk :as walk])
+  (:require [clojure
+             [set :as set]
+             [string :as s]
+             [walk :as walk]]
             [clojure.tools.logging :as log]
-            (honeysql [core :as hsql]
-                      [helpers :as h])
-            [metabase.config :as config]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            [metabase.driver.google :as google]
-            [metabase.driver.generic-sql :as sql]
+            [honeysql
+             [core :as hsql]
+             [helpers :as h]]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [util :as u]]
+            [metabase.driver
+             [generic-sql :as sql]
+             [google :as google]]
             [metabase.driver.generic-sql.query-processor :as sqlqp]
             [metabase.driver.generic-sql.util.unprepare :as unprepare]
-            (metabase.models [database :refer [Database]]
-                             [field :as field]
-                             [table :as table])
-            [metabase.sync-database.analyze :as analyze]
-            metabase.query-processor.interface
+            [metabase.models
+             [database :refer [Database]]
+             [field :as field]
+             [table :as table]]
             [metabase.query-processor.util :as qputil]
-            [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx])
-  (:import (java.util Collections Date)
-           (com.google.api.client.googleapis.auth.oauth2 GoogleCredential GoogleCredential$Builder GoogleAuthorizationCodeFlow GoogleAuthorizationCodeFlow$Builder GoogleTokenResponse)
-           com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
-           (com.google.api.client.googleapis.json GoogleJsonError GoogleJsonResponseException)
-           com.google.api.client.googleapis.services.AbstractGoogleClientRequest
-           com.google.api.client.http.HttpTransport
-           com.google.api.client.json.JsonFactory
-           com.google.api.client.json.jackson2.JacksonFactory
-           (com.google.api.services.bigquery Bigquery Bigquery$Builder BigqueryScopes)
-           (com.google.api.services.bigquery.model Table TableCell TableFieldSchema TableList TableList$Tables TableReference TableRow TableSchema QueryRequest QueryResponse)
-           (metabase.query_processor.interface DateTimeValue Value)))
-
+            [metabase.sync-database.analyze :as analyze]
+            [metabase.util.honeysql-extensions :as hx]
+            [toucan.db :as db])
+  (:import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
+           [com.google.api.services.bigquery Bigquery Bigquery$Builder BigqueryScopes]
+           [com.google.api.services.bigquery.model QueryRequest QueryResponse Table TableCell TableFieldSchema TableList TableList$Tables TableReference TableRow TableSchema]
+           [java.util Collections Date]
+           [metabase.query_processor.interface DateTimeValue Value]))
 
 ;;; ------------------------------------------------------------ Client ------------------------------------------------------------
 
diff --git a/src/metabase/driver/crate.clj b/src/metabase/driver/crate.clj
index 55082daf911962089bfc19d9a3e481c7ff75aa36..1332754e049310b285a6c693e79ecaee5b8fc134 100644
--- a/src/metabase/driver/crate.clj
+++ b/src/metabase/driver/crate.clj
@@ -2,10 +2,11 @@
   (:require [clojure.java.jdbc :as jdbc]
             [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
-            [metabase.driver :as driver]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.crate.util :as crate-util]
-            [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u])
+            [metabase.driver.generic-sql :as sql])
   (:import java.sql.DatabaseMetaData))
 
 (def ^:private ^:const column->base-type
diff --git a/src/metabase/driver/druid.clj b/src/metabase/driver/druid.clj
index 1cfe762537c4f60b095724370b24c44a2de2a3ef..1ab5281208d07a006b326cee262abc06fb085c90 100644
--- a/src/metabase/driver/druid.clj
+++ b/src/metabase/driver/druid.clj
@@ -1,14 +1,16 @@
 (ns metabase.driver.druid
   "Druid driver."
-  (:require [clojure.tools.logging :as log]
+  (:require [cheshire.core :as json]
             [clj-http.client :as http]
-            [cheshire.core :as json]
-            [metabase.driver :as driver]
+            [clojure.tools.logging :as log]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.druid.query-processor :as qp]
-            (metabase.models [field :as field]
-                             [table :as table])
-            [metabase.sync-database.analyze :as analyze]
-            [metabase.util :as u]))
+            [metabase.models
+             [field :as field]
+             [table :as table]]
+            [metabase.sync-database.analyze :as analyze]))
 
 ;;; ### Request helper fns
 
diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj
index 8befd598664fa16eb7084a10eafe76a478045cd7..c0f51dd65358152cef264c00a71049612542814e 100644
--- a/src/metabase/driver/druid/query_processor.clj
+++ b/src/metabase/driver/druid/query_processor.clj
@@ -1,22 +1,15 @@
 (ns metabase.driver.druid.query-processor
-  (:require [clojure.core.match :refer [match]]
+  (:require [cheshire.core :as json]
+            [clojure.core.match :refer [match]]
             [clojure.math.numeric-tower :as math]
             [clojure.string :as s]
             [clojure.tools.logging :as log]
-            [cheshire.core :as json]
             [metabase.driver.druid.js :as js]
-            (metabase.query-processor [annotate :as annotate]
-                                      [interface :as i])
+            [metabase.query-processor
+             [annotate :as annotate]
+             [interface :as i]]
             [metabase.util :as u])
-  (:import clojure.lang.Keyword
-           (metabase.query_processor.interface AgFieldRef
-                                               DateTimeField
-                                               DateTimeValue
-                                               Expression
-                                               Field
-                                               RelativeDateTimeValue
-                                               Value)))
-
+  (:import [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Expression Field RelativeDateTimeValue Value]))
 
 ;;             +-----> ::select      +----> :groupBy
 ;; ::query ----|                     |
diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index dbec2699e8e5f9c135fab69c7843b3bfcb5edf3b..2eedae05f22be37cd57e374a6bc307d262ddc7c3 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -1,26 +1,28 @@
 (ns metabase.driver.generic-sql
-  (:require [clojure.java.jdbc :as jdbc]
+  (:require [clojure
+             [set :as set]
+             [string :as str]]
+            [clojure.java.jdbc :as jdbc]
             [clojure.math.numeric-tower :as math]
-            (clojure [set :as set]
-                     [string :as str])
             [clojure.tools.logging :as log]
-            (honeysql [core :as hsql]
-                      [format :as hformat])
-            (metabase [db :as db]
-                      [driver :as driver])
-            (metabase.models [field :as field]
-                             raw-table
-                             [table :as table])
-            metabase.query-processor.interface
+            [honeysql
+             [core :as hsql]
+             [format :as hformat]]
+            [metabase
+             [db :as db]
+             [driver :as driver]
+             [util :as u]]
+            [metabase.models
+             [field :as field]
+             [table :as table]]
             [metabase.sync-database.analyze :as analyze]
-            [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx])
-  (:import (java.sql DatabaseMetaData ResultSet)
-           java.util.Map
-           (clojure.lang Keyword PersistentVector)
+  (:import [clojure.lang Keyword PersistentVector]
            com.mchange.v2.c3p0.ComboPooledDataSource
+           [java.sql DatabaseMetaData ResultSet]
+           java.util.Map
            metabase.models.field.FieldInstance
-           (metabase.query_processor.interface Field Value)))
+           [metabase.query_processor.interface Field Value]))
 
 (defprotocol ISQLDriver
   "Methods SQL-based drivers should implement in order to use `IDriverSQLDefaultsMixin`.
@@ -106,9 +108,10 @@
         (hsql/format ... :quoting (quote-style driver))")
 
   (set-timezone-sql ^String [this]
-    "*OPTIONAL*. This should be a prepared JDBC SQL statement string to be used to set the timezone for the current transaction.
+    "*OPTIONAL*. This should be a format string containing a SQL statement to be used to set the timezone for the current transaction.
+     The `%s` will be replaced with a string literal for a timezone, e.g. `US/Pacific`.
 
-       \"SET @@session.timezone = ?;\"")
+       \"SET @@session.timezone = %s;\"")
 
   (stddev-fn ^clojure.lang.Keyword [this]
     "*OPTIONAL*. Keyword name of the SQL function that should be used to do a standard deviation aggregation. Defaults to `:STDDEV`.")
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index 741a791a1a35a4ea6836755639fe631c7b6bd9da..c205b0c11c34bcf57db553641bcc86031ce3fba0 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -1,33 +1,22 @@
 (ns metabase.driver.generic-sql.query-processor
   "The Query Processor is responsible for translating the Metabase Query Language into HoneySQL SQL forms."
   (:require [clojure.java.jdbc :as jdbc]
-            (clojure [string :as s]
-                     [walk :as walk])
             [clojure.tools.logging :as log]
-            (honeysql [core :as hsql]
-                      [format :as hformat]
-                      [helpers :as h]
-                      types)
-            (metabase [config :as config]
-                      [driver :as driver])
+            [honeysql
+             [core :as hsql]
+             [format :as hformat]
+             [helpers :as h]]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.generic-sql :as sql]
-            (metabase.query-processor [annotate :as annotate]
-                                      [interface :as i]
-                                      [util :as qputil])
-            [metabase.util :as u]
+            [metabase.query-processor
+             [annotate :as annotate]
+             [interface :as i]
+             [util :as qputil]]
             [metabase.util.honeysql-extensions :as hx])
-  (:import java.sql.Timestamp
-           java.util.Date
-           clojure.lang.Keyword
-           (metabase.query_processor.interface AgFieldRef
-                                               DateTimeField
-                                               DateTimeValue
-                                               Field
-                                               Expression
-                                               ExpressionRef
-                                               JoinTable
-                                               RelativeDateTimeValue
-                                               Value)))
+  (:import clojure.lang.Keyword
+           [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Expression ExpressionRef Field RelativeDateTimeValue Value]))
 
 (def ^:dynamic *query*
   "The outer query currently being processed."
@@ -340,10 +329,12 @@
 (defn- set-timezone!
   "Set the timezone for the current connection."
   [driver settings connection]
-  (let [timezone (:report-timezone settings)
-        sql      (sql/set-timezone-sql driver)]
-    (log/debug (u/pprint-to-str 'green [sql timezone]))
-    (jdbc/db-do-prepared connection [sql timezone])))
+  (let [timezone      (u/prog1 (:report-timezone settings)
+                        (assert (re-matches #"[A-Za-z\/_]+" <>)))
+        format-string (sql/set-timezone-sql driver)
+        sql           (format format-string (str \' timezone \'))]
+    (log/debug (u/format-color 'green "Setting timezone with statement: %s" sql))
+    (jdbc/db-do-prepared connection [sql])))
 
 (defn- run-query-without-timezone [driver settings connection query]
   (do-in-transaction connection (partial run-query query)))
diff --git a/src/metabase/driver/google.clj b/src/metabase/driver/google.clj
index 4dee83b195e5246d435c0af376e4dfddfe4f5e27..5514dca3eaaf1c4cdaf0b3066fe75e5d6fb2b021 100644
--- a/src/metabase/driver/google.clj
+++ b/src/metabase/driver/google.clj
@@ -1,18 +1,18 @@
 (ns metabase.driver.google
   "Shared logic for various Google drivers, including BigQuery and Google Analytics."
   (:require [clojure.tools.logging :as log]
-            [metabase.config :as config]
-            [toucan.db :as db]
+            [metabase
+             [config :as config]
+             [util :as u]]
             [metabase.models.database :refer [Database]]
-            [metabase.util :as u])
-  (:import java.util.Collections
-           (com.google.api.client.googleapis.auth.oauth2 GoogleCredential GoogleCredential$Builder GoogleAuthorizationCodeFlow GoogleAuthorizationCodeFlow$Builder GoogleTokenResponse)
+            [toucan.db :as db])
+  (:import [com.google.api.client.googleapis.auth.oauth2 GoogleAuthorizationCodeFlow GoogleAuthorizationCodeFlow$Builder GoogleCredential GoogleCredential$Builder GoogleTokenResponse]
            com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
-           (com.google.api.client.googleapis.json GoogleJsonError GoogleJsonResponseException)
+           [com.google.api.client.googleapis.json GoogleJsonError GoogleJsonResponseException]
            com.google.api.client.googleapis.services.AbstractGoogleClientRequest
            com.google.api.client.http.HttpTransport
-           com.google.api.client.json.JsonFactory
-           com.google.api.client.json.jackson2.JacksonFactory))
+           com.google.api.client.json.jackson2.JacksonFactory
+           com.google.api.client.json.JsonFactory))
 
 (def ^HttpTransport http-transport
   "`HttpTransport` for use with Google drivers."
diff --git a/src/metabase/driver/googleanalytics.clj b/src/metabase/driver/googleanalytics.clj
index 02b092d937430fa246d295de1fb2f5cb12eb02c8..e2b0cb51af1dfaee89d85ee8add22c9e050de779 100644
--- a/src/metabase/driver/googleanalytics.clj
+++ b/src/metabase/driver/googleanalytics.clj
@@ -1,32 +1,16 @@
 (ns metabase.driver.googleanalytics
-  ;; TODO - probably makes to call this namespace `google-analytics`
-  (:require (clojure [set :as set]
-                     [string :as s]
-                     [walk :as walk])
-            [clojure.tools.logging :as log]
-            [cheshire.core :as json]
-            [metabase.config :as config]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
+  (:require [cheshire.core :as json]
+            [clojure.string :as s]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.google :as google]
-            (metabase.driver.googleanalytics [query-processor :as qp])
-            (metabase.models [database :refer [Database]]
-                             [field :as field]
-                             [table :as table])
-            [metabase.sync-database.analyze :as analyze]
-            metabase.query-processor.interface
-            [metabase.util :as u])
-  (:import (java.util Collections Date List Map)
-           (com.google.api.client.googleapis.auth.oauth2 GoogleCredential GoogleCredential$Builder GoogleAuthorizationCodeFlow GoogleAuthorizationCodeFlow$Builder GoogleTokenResponse)
-           com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
-           (com.google.api.client.googleapis.json GoogleJsonError GoogleJsonResponseException)
-           com.google.api.client.googleapis.services.AbstractGoogleClientRequest
-           com.google.api.client.http.HttpTransport
-           com.google.api.client.json.JsonFactory
-           com.google.api.client.json.jackson2.JacksonFactory
-           (com.google.api.services.analytics Analytics Analytics$Builder Analytics$Data$Ga$Get AnalyticsScopes)
-           (com.google.api.services.analytics.model Account Accounts Columns Column Profile Profiles Webproperty Webproperties)))
-
+            [metabase.driver.googleanalytics.query-processor :as qp]
+            [metabase.models.database :refer [Database]])
+  (:import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
+           [com.google.api.services.analytics Analytics Analytics$Builder Analytics$Data$Ga$Get AnalyticsScopes]
+           [com.google.api.services.analytics.model Column Columns Profile Profiles Webproperties Webproperty]
+           [java.util Collections Date Map]))
 
 ;;; ------------------------------------------------------------ Client ------------------------------------------------------------
 
diff --git a/src/metabase/driver/googleanalytics/query_processor.clj b/src/metabase/driver/googleanalytics/query_processor.clj
index 25db1a5213f4bc3d22d667c23ee3f83856acb8c1..1424d4d7ae115dd6d4e63ad5d0af27b6be086d13 100644
--- a/src/metabase/driver/googleanalytics/query_processor.clj
+++ b/src/metabase/driver/googleanalytics/query_processor.clj
@@ -1,21 +1,12 @@
 (ns metabase.driver.googleanalytics.query-processor
   "The Query Processor is responsible for translating the Metabase Query Language into Google Analytics request format."
-  (:require (clojure [string :as s])
-            [clojure.tools.logging :as log]
+  (:require [clojure.string :as s]
             [clojure.tools.reader.edn :as edn]
             [medley.core :as m]
             [metabase.query-processor.expand :as ql]
             [metabase.util :as u])
-  (:import java.sql.Timestamp
-           java.util.Date
-           clojure.lang.PersistentArrayMap
-           (com.google.api.services.analytics.model GaData GaData$ColumnHeaders)
-           (metabase.query_processor.interface AgFieldRef
-                                               DateTimeField
-                                               DateTimeValue
-                                               Field
-                                               RelativeDateTimeValue
-                                               Value)))
+  (:import [com.google.api.services.analytics.model GaData GaData$ColumnHeaders]
+           [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Field RelativeDateTimeValue Value]))
 
 (def ^:private ^:const earliest-date "2005-01-01")
 (def ^:private ^:const latest-date "today")
diff --git a/src/metabase/driver/h2.clj b/src/metabase/driver/h2.clj
index 1cd82657fa7b846e85494811f16c5a74c9646571..ff35bb0c5b0bc6f4e877983706d367f18cba9d45 100644
--- a/src/metabase/driver/h2.clj
+++ b/src/metabase/driver/h2.clj
@@ -1,15 +1,15 @@
 (ns metabase.driver.h2
-  (:require [clojure.java.jdbc :as jdbc]
-            [clojure.string :as s]
+  (:require [clojure.string :as s]
             [honeysql.core :as hsql]
-            [toucan.db :as db]
-            [metabase.db :as mdb]
+            [metabase
+             [db :as mdb]
+             [driver :as driver]
+             [util :as u]]
             [metabase.db.spec :as dbspec]
-            [metabase.driver :as driver]
             [metabase.driver.generic-sql :as sql]
             [metabase.models.database :refer [Database]]
-            [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx]))
+            [metabase.util.honeysql-extensions :as hx]
+            [toucan.db :as db]))
 
 (def ^:private ^:const column->base-type
   {:ARRAY                       :type/*
diff --git a/src/metabase/driver/mongo.clj b/src/metabase/driver/mongo.clj
index 688cc3448fc3896ce26d59fd236f933eb39d70c5..dbe90592acd56f9bf9932c5dfb2d1b4f38bf7f35 100644
--- a/src/metabase/driver/mongo.clj
+++ b/src/metabase/driver/mongo.clj
@@ -1,22 +1,26 @@
 (ns metabase.driver.mongo
   "MongoDB Driver."
-  (:require [clojure.string :as s]
+  (:require [cheshire.core :as json]
+            [clojure.string :as s]
             [clojure.tools.logging :as log]
-            [cheshire.core :as json]
-            (monger [collection :as mc]
-                    [command :as cmd]
-                    [conversion :as conv]
-                    [db :as mdb]
-                    [query :as mq])
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            (metabase.driver.mongo [query-processor :as qp]
-                                   [util :refer [*mongo-connection* with-mongo-connection values->base-type]])
-            (metabase.models [database :refer [Database]]
-                             [field :as field]
-                             [table :as table])
+            [metabase
+             [driver :as driver]
+             [util :as u]]
+            [metabase.driver.mongo
+             [query-processor :as qp]
+             [util :refer [*mongo-connection* with-mongo-connection]]]
+            [metabase.models
+             [database :refer [Database]]
+             [field :as field]
+             [table :as table]]
             [metabase.sync-database.analyze :as analyze]
-            [metabase.util :as u])
+            [monger
+             [collection :as mc]
+             [command :as cmd]
+             [conversion :as conv]
+             [db :as mdb]
+             [query :as mq]]
+            [toucan.db :as db])
   (:import com.mongodb.DB))
 
 ;;; ## MongoDriver
diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj
index 4dac3f89814c8677bfd63ebbcaad925229b68545..755c7c4e037ede505c6820f8017d94a1f537c1af 100644
--- a/src/metabase/driver/mongo/query_processor.clj
+++ b/src/metabase/driver/mongo/query_processor.clj
@@ -1,30 +1,24 @@
 (ns metabase.driver.mongo.query-processor
   (:refer-clojure :exclude [find sort])
-  (:require (clojure [set :as set]
-                     [string :as s])
+  (:require [cheshire.core :as json]
+            [clojure
+             [set :as set]
+             [string :as s]
+             [walk :as walk]]
             [clojure.tools.logging :as log]
-            [clojure.walk :as walk]
-            [cheshire.core :as json]
-            (monger [collection :as mc]
-                    joda-time                ; apparently if this is loaded Monger can handle JodaTime dates (?)
-                    [operators :refer :all])
-            [metabase.driver.mongo.util :refer [with-mongo-connection *mongo-connection* values->base-type]]
-            [metabase.models.table :refer [Table]]
-            (metabase.query-processor [annotate :as annotate]
-                                      [interface :as i])
-            [metabase.util :as u])
+            [metabase.driver.mongo.util :refer [*mongo-connection*]]
+            [metabase.query-processor
+             [annotate :as annotate]
+             [interface :as i]]
+            [metabase.util :as u]
+            [monger joda-time
+             [collection :as mc]
+             [operators :refer :all]])
   (:import java.sql.Timestamp
            java.util.Date
-           clojure.lang.PersistentArrayMap
-           (com.mongodb CommandResult DB)
+           [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Field RelativeDateTimeValue Value]
            org.bson.types.ObjectId
-           org.joda.time.DateTime
-           (metabase.query_processor.interface AgFieldRef
-                                               DateTimeField
-                                               DateTimeValue
-                                               Field
-                                               RelativeDateTimeValue
-                                               Value)))
+           org.joda.time.DateTime))
 
 (def ^:private ^:const $subtract :$subtract)
 
diff --git a/src/metabase/driver/mongo/util.clj b/src/metabase/driver/mongo/util.clj
index 52e93eb088a8ac62476bae4d5fe998e8b6a0ea44..e790e1be493d5eccf26b41dad9be7600f4dc5e57 100644
--- a/src/metabase/driver/mongo/util.clj
+++ b/src/metabase/driver/mongo/util.clj
@@ -1,10 +1,12 @@
 (ns metabase.driver.mongo.util
   "`*mongo-connection*`, `with-mongo-connection`, and other functions shared between several Mongo driver namespaces."
   (:require [clojure.tools.logging :as log]
-            (monger [core :as mg]
-                    [credentials :as mcred])
-            (metabase [driver :as driver]
-                      [util :as u])))
+            [metabase
+             [driver :as driver]
+             [util :as u]]
+            [monger
+             [core :as mg]
+             [credentials :as mcred]]))
 
 (def ^:const ^:private connection-timeout-ms
   "Number of milliseconds to wait when attempting to establish a Mongo connection.
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index 100576a657fd9963ad0fc7b354e0dcca4b542e92..9df960b14a26be5442da304ae77735582d916a7c 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -1,11 +1,13 @@
 (ns metabase.driver.mysql
-  (:require (clojure [set :as set]
-                     [string :as s])
+  (:require [clojure
+             [set :as set]
+             [string :as s]]
             [honeysql.core :as hsql]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.db.spec :as dbspec]
-            [metabase.driver :as driver]
             [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx]))
 
 ;;; # IMPLEMENTATION
@@ -189,7 +191,7 @@
           ;; run the command `mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql`
           ;; See https://dev.mysql.com/doc/refman/5.7/en/time-zone-support.html for details
           ;; TODO - This can also be set via `sessionVariables` in the connection string, if that's more useful (?)
-          :set-timezone-sql          (constantly "SET @@session.time_zone = ?;")
+          :set-timezone-sql          (constantly "SET @@session.time_zone = %s;")
           :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
 
 (driver/register-driver! :mysql (MySQLDriver.))
diff --git a/src/metabase/driver/oracle.clj b/src/metabase/driver/oracle.clj
index 1780dfe544ab3b995dcecacdefe58addb23f01a9..c78447398ed9b28734cdbb2bb6b1a3af434446a7 100644
--- a/src/metabase/driver/oracle.clj
+++ b/src/metabase/driver/oracle.clj
@@ -1,16 +1,13 @@
 (ns metabase.driver.oracle
   (:require [clojure.java.jdbc :as jdbc]
-            (clojure [set :as set]
-                     [string :as s])
-            [clojure.tools.logging :as log]
-            (honeysql [core :as hsql]
-                      [helpers :as h])
-            [metabase.config :as config]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
+            [clojure.set :as set]
+            [honeysql.core :as hsql]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.generic-sql :as sql]
             [metabase.driver.generic-sql.query-processor :as sqlqp]
-            [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx]))
 
 (def ^:private ^:const pattern->type
@@ -257,9 +254,7 @@
                                           (require 'metabase.test.data.oracle)
                                           ((resolve 'metabase.test.data.oracle/non-session-schemas)))))
           :field-percent-urls        sql/slow-field-percent-urls
-          ;; TODO - we *should* be able to set timezone using the SQL below, but I think the SQL doesn't work with prepared params (i.e., '?')
-          ;; Find some way to work around this for Oracle
-          ;; :set-timezone-sql          (constantly "ALTER session SET time_zone = ?")
+          :set-timezone-sql          (constantly "ALTER session SET time_zone = %s")
           :prepare-value             (u/drop-first-arg prepare-value)
           :string-length-fn          (u/drop-first-arg string-length-fn)
           :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index 1e3c008d98d0ae41b3f60a92a8b710f7659f3fd7..765f8690846d3e08b166a2c68ae074ef71a3e15b 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -1,19 +1,15 @@
 (ns metabase.driver.postgres
-  ;; TODO - rework this to be like newer-style namespaces that use `u/drop-first-arg`
-  (:require [clojure.java.jdbc :as jdbc]
-            (clojure [set :refer [rename-keys], :as set]
-                     [string :as s])
-            [clojure.tools.logging :as log]
+  (:require [clojure
+             [set :as set :refer [rename-keys]]
+             [string :as s]]
             [honeysql.core :as hsql]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.db.spec :as dbspec]
-            [metabase.driver :as driver]
             [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx])
-  ;; This is necessary for when NonValidatingFactory is passed in the sslfactory connection string argument,
-  ;; e.x. when connecting to a Heroku Postgres database from outside of Heroku.
-  (:import java.util.UUID
-           org.postgresql.ssl.NonValidatingFactory))
+  (:import java.util.UUID))
 
 (def ^:private ^:const column->base-type
   "Map of Postgres column types -> Field base types.
@@ -196,7 +192,7 @@
           :connection-details->spec  (u/drop-first-arg connection-details->spec)
           :date                      (u/drop-first-arg date)
           :prepare-value             (u/drop-first-arg prepare-value)
-          :set-timezone-sql          (constantly "UPDATE pg_settings SET setting = ? WHERE name ILIKE 'timezone';")
+          :set-timezone-sql          (constantly "SET SESSION TIMEZONE TO %s;")
           :string-length-fn          (u/drop-first-arg string-length-fn)
           :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
 
diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj
index 97630c44ff7b08284a7b8d5a11700d335207def3..07a69fdec585df886841fd910853b32074f0917b 100644
--- a/src/metabase/driver/presto.clj
+++ b/src/metabase/driver/presto.clj
@@ -1,22 +1,25 @@
 (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]
+  (:require [clj-http.client :as http]
+            [clojure
+             [set :as set]
+             [string :as str]]
+            [honeysql
+             [core :as hsql]
+             [helpers :as h]]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [util :as u]]
             [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.models
+             [field :as field]
+             [table :as table]]
             [metabase.query-processor.util :as qputil]
-            [metabase.util :as u]
+            [metabase.sync-database.analyze :as analyze]
             [metabase.util.honeysql-extensions :as hx])
   (:import java.util.Date
-           (metabase.query_processor.interface DateTimeValue Value)))
-
+           [metabase.query_processor.interface DateTimeValue Value]))
 
 ;;; Presto API helpers
 
diff --git a/src/metabase/driver/redshift.clj b/src/metabase/driver/redshift.clj
index 08ebe41f5dac576be673bc5e7585ca6637099420..a9c833029c6c0c041bf8294c102d2b24d52858a3 100644
--- a/src/metabase/driver/redshift.clj
+++ b/src/metabase/driver/redshift.clj
@@ -3,12 +3,14 @@
   (:require [clojure.java.jdbc :as jdbc]
             [clojure.string :as str]
             [honeysql.core :as hsql]
-            [metabase.config :as config]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [util :as u]]
             [metabase.db.spec :as dbspec]
-            [metabase.driver :as driver]
-            (metabase.driver [generic-sql :as sql]
-                             [postgres :as postgres])
-            [metabase.util :as u]
+            [metabase.driver
+             [generic-sql :as sql]
+             [postgres :as postgres]]
             [metabase.util.honeysql-extensions :as hx]))
 
 (defn- connection-details->spec [details]
diff --git a/src/metabase/driver/sqlite.clj b/src/metabase/driver/sqlite.clj
index 3aa4092e92257da90cef4539c703ef22842d6a76..9b07a7c53d063ca558c395e979db989e4c970ee3 100644
--- a/src/metabase/driver/sqlite.clj
+++ b/src/metabase/driver/sqlite.clj
@@ -1,12 +1,15 @@
 (ns metabase.driver.sqlite
-  (:require (clojure [set :as set]
-                     [string :as s])
-            (honeysql [core :as hsql]
-                      [format :as hformat])
-            [metabase.config :as config]
-            [metabase.driver :as driver]
+  (:require [clojure
+             [set :as set]
+             [string :as s]]
+            [honeysql
+             [core :as hsql]
+             [format :as hformat]]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx]))
 
 (defn- connection-details->spec
diff --git a/src/metabase/driver/sqlserver.clj b/src/metabase/driver/sqlserver.clj
index e92f37a9711495e412d990a3cfae1eed3b7a2ec9..ec75ae4630c152ebd28a64e8be130cb874d0c42d 100644
--- a/src/metabase/driver/sqlserver.clj
+++ b/src/metabase/driver/sqlserver.clj
@@ -1,11 +1,12 @@
 (ns metabase.driver.sqlserver
-  (:require [clojure.string :as s]
-            [honeysql.core :as hsql]
-            [metabase.driver :as driver]
+  (:require [honeysql.core :as hsql]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx])
-  (:import net.sourceforge.jtds.jdbc.Driver)) ; need to import this in order to load JDBC driver
+            [metabase.util.honeysql-extensions :as hx]))
+
+ ; need to import this in order to load JDBC driver
 
 (defn- column->base-type
   "See [this page](https://msdn.microsoft.com/en-us/library/ms187752.aspx) for details."
diff --git a/src/metabase/driver/vertica.clj b/src/metabase/driver/vertica.clj
index 675e815ff7aa97d1a1c88205ab561a3663638332..5e20ed3c4d514926fa965866302ef94b0c1441f1 100644
--- a/src/metabase/driver/vertica.clj
+++ b/src/metabase/driver/vertica.clj
@@ -1,12 +1,12 @@
 (ns metabase.driver.vertica
   (:require [clojure.java.jdbc :as jdbc]
-            (clojure [set :refer [rename-keys], :as set]
-                     [string :as s])
+            [clojure.set :as set]
             [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
-            [metabase.driver :as driver]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.driver.generic-sql :as sql]
-            [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx]))
 
 (def ^:private ^:const column->base-type
@@ -127,7 +127,7 @@
          {:column->base-type         (u/drop-first-arg column->base-type)
           :connection-details->spec  (u/drop-first-arg connection-details->spec)
           :date                      (u/drop-first-arg date)
-          :set-timezone-sql          (constantly "SET TIME ZONE TO ?;")
+          :set-timezone-sql          (constantly "SET TIME ZONE TO %s;")
           :string-length-fn          (u/drop-first-arg string-length-fn)
           :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
 
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 9731b50acdbdae802f760dda254ccf090546ef74..62933ce0174a73bafa9e879399350b5e7fc06dfd 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -1,9 +1,10 @@
 (ns metabase.email
   (:require [clojure.tools.logging :as log]
-            (postal [core :as postal]
-                    [support :refer [make-props]])
-            [metabase.models.setting :refer [defsetting], :as setting]
-            [metabase.util :as u])
+            [metabase.models.setting :as setting :refer [defsetting]]
+            [metabase.util :as u]
+            [postal
+             [core :as postal]
+             [support :refer [make-props]]])
   (:import javax.mail.Session))
 
 ;;; CONFIG
@@ -15,11 +16,11 @@
 (defsetting email-smtp-password "SMTP password.")
 (defsetting email-smtp-port     "The port your SMTP server uses for outgoing emails.")
 (defsetting email-smtp-security
-  "SMTP secure connection protocol. (tls, ssl, or none)"
+  "SMTP secure connection protocol. (tls, ssl, starttls, or none)"
   :default "none"
   :setter  (fn [new-value]
              (when-not (nil? new-value)
-               (assert (contains? #{"tls" "ssl" "none"} new-value)))
+               (assert (contains? #{"tls" "ssl" "none" "starttls"} new-value)))
              (setting/set-string! :email-smtp-security new-value)))
 
 ;; ## PUBLIC INTERFACE
@@ -38,6 +39,8 @@
   (merge m (case (keyword ssl-setting)
              :tls {:tls true}
              :ssl {:ssl true}
+             :starttls {:starttls.enable true
+                        :starttls.required true}
              {})))
 
 (defn- smtp-settings []
@@ -85,27 +88,19 @@
       {:error   :ERROR
        :message (.getMessage e)})))
 
-
-(defn test-smtp-connection
-  "Test the connection to an SMTP server to determine if we can send emails.
-
-   Takes in a dictionary of properties such as:
-       {:host     \"localhost\"
-        :port     587
-        :user     \"bigbird\"
-        :pass     \"luckyme\"
-        :sender   \"foo@mycompany.com\"
-        :security \"tls\"}"
+(defn- run-smtp-test
+  "tests an SMTP configuration by attempting to connect and authenticate
+   if an authenticated method is passed in :security."
   [{:keys [host port user pass sender security] :as details}]
   {:pre [(string? host)
          (integer? port)]}
   (try
-    (let [ssl?    (= security "ssl")
-          proto   (if ssl? "smtps" "smtp")
+    (let [ssl?      (= security "ssl")
+          proto     (if ssl? "smtps" "smtp")
           details (-> details
                       (assoc :proto proto
                              :connectiontimeout "1000"
-                             :timeout "1000")
+                             :timeout "4000")
                       (add-ssl-settings security))
           session (doto (Session/getInstance (make-props sender details))
                     (.setDebug false))]
@@ -117,3 +112,42 @@
       (log/error "Error testing SMTP connection:" (.getMessage e))
       {:error   :ERROR
        :message (.getMessage e)})))
+
+(def ^:private email-security-order ["tls" "starttls" "ssl"])
+
+(defn- guess-smtp-security
+  "Attempts to use each of the security methods in security order with the same set of credentials.
+   This is used only when the initial connection attempt fails, so it won't overwrite a functioning
+   configuration. If this uses something other than the provided method, a warning gets printed on
+   the config page"
+  [details]
+  (loop [[security-type & more-to-try] email-security-order] ;; make sure this is not lazy, or chunking
+    (when security-type                                      ;; can cause some servers to block requests
+      (let [test-result (run-smtp-test (assoc details :security security-type))]
+        (if (not= :ERROR (:error test-result))
+          (assoc test-result :security security-type)
+          (do
+            (Thread/sleep 500) ;; try not to get banned from outlook.com
+            (recur more-to-try)))))))
+
+(defn test-smtp-connection
+  "Test the connection to an SMTP server to determine if we can send emails.
+
+   Takes in a dictionary of properties such as:
+       {:host     \"localhost\"
+        :port     587
+        :user     \"bigbird\"
+        :pass     \"luckyme\"
+        :sender   \"foo@mycompany.com\"
+        :security \"tls\"}"
+  [details]
+  (let [inital-attempt (run-smtp-test details)
+        it-worked?     (= :SUCCESS (:error inital-attempt))
+        attempted-fix  (if (not it-worked?)
+                         (guess-smtp-security details))
+        we-fixed-it?     (= :SUCCESS (:error attempted-fix))]
+    (if it-worked?
+      inital-attempt
+      (if we-fixed-it?
+        attempted-fix
+        inital-attempt))))
diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj
index 6febb9706b0c76a676cba347685229d59d434a17..d043f52df2a0a575cf364a9ed9d00e6ce81e2617 100644
--- a/src/metabase/email/messages.clj
+++ b/src/metabase/email/messages.clj
@@ -4,17 +4,20 @@
   (:require [clojure.core.cache :as cache]
             [hiccup.core :refer [html]]
             [medley.core :as m]
-            (stencil [core :as stencil]
-                     [loader :as stencil-loader])
-            [toucan.db :as db]
-            (metabase [config :as config]
-                      [email :as email])
-            [metabase.public-settings :as public-settings]
+            [metabase
+             [config :as config]
+             [email :as email]
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.pulse.render :as render]
-            [metabase.util :as u]
-            (metabase.util [quotation :as quotation]
-                           [urls :as url]))
-  (:import (java.io File FileOutputStream)
+            [metabase.util
+             [quotation :as quotation]
+             [urls :as url]]
+            [stencil
+             [core :as stencil]
+             [loader :as stencil-loader]]
+            [toucan.db :as db])
+  (:import [java.io File FileOutputStream]
            java.util.Arrays))
 
 ;; Dev only -- disable template caching
diff --git a/src/metabase/email/user_joined_notification.mustache b/src/metabase/email/user_joined_notification.mustache
index a9ac35cc4ca336ef75d81d2c92295a9447538736..25f5ca52076ad185a8efc3d8496716e19deca158 100644
--- a/src/metabase/email/user_joined_notification.mustache
+++ b/src/metabase/email/user_joined_notification.mustache
@@ -4,7 +4,7 @@
       <h2 style="font-weight: normal; color: #4C545B;line-height: 1.65rem;">
         {{joinedUserName}}
         {{#joinedViaSSO}}created a Metabase account.{{/joinedViaSSO}}
-        {{^joinedViaSSO}}accepted your Metabase invitation.{{/joinedViaSSO}}
+        {{^joinedViaSSO}}accepted their Metabase invitation.{{/joinedViaSSO}}
       </h2>
       <h4 style="font-weight: normal;">
         <a style="color: #4A90E2; text-decoration: none;" href="mailto:{{adminEmail}}">
diff --git a/src/metabase/events/activity_feed.clj b/src/metabase/events/activity_feed.clj
index 113bdb0617eca8d46bb1f8e4dd1d6a1abfc98282..878a30038731898ea14f5b62647146dbd450986a 100644
--- a/src/metabase/events/activity_feed.clj
+++ b/src/metabase/events/activity_feed.clj
@@ -1,15 +1,13 @@
 (ns metabase.events.activity-feed
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
             [metabase.events :as events]
-            (metabase.models [activity :refer [Activity], :as activity]
-                             [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [interface :as mi]
-                             [session :refer [Session first-session-for-user]]
-                             [table :as table])))
-
+            [metabase.models
+             [activity :as activity :refer [Activity]]
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [table :as table]]
+            [toucan.db :as db]))
 
 (def ^:const activity-feed-topics
   "The `Set` of event topics which are subscribed to for use in the Metabase activity feed."
diff --git a/src/metabase/events/dependencies.clj b/src/metabase/events/dependencies.clj
index 9cf2fc591ce6f5793ccb927dcd315b2511a133a2..19cb27bba03479884c2eba982aa73deded2a35b7 100644
--- a/src/metabase/events/dependencies.clj
+++ b/src/metabase/events/dependencies.clj
@@ -2,10 +2,10 @@
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
             [metabase.events :as events]
-            (metabase.models [card :refer [Card]]
-                             [dependency :refer [IDependent] :as dependency]
-                             [metric :refer [Metric]])))
-
+            [metabase.models
+             [card :refer [Card]]
+             [dependency :as dependency :refer [IDependent]]
+             [metric :refer [Metric]]]))
 
 (def ^:private ^:const dependencies-topics
   "The `Set` of event topics which are subscribed to for use in dependencies tracking."
diff --git a/src/metabase/events/driver_notifications.clj b/src/metabase/events/driver_notifications.clj
index 51bb07abf931ddd8bf54a97132cc8047567b8594..734411b4e3a8e9a79c0633565486d7894e5e233f 100644
--- a/src/metabase/events/driver_notifications.clj
+++ b/src/metabase/events/driver_notifications.clj
@@ -1,11 +1,9 @@
 (ns metabase.events.driver-notifications
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            [metabase.events :as events]
-            [metabase.models.database :refer [Database]]))
-
+            [metabase
+             [driver :as driver]
+             [events :as events]]))
 
 (def ^:const ^:private driver-notifications-topics
   "The `Set` of event topics which are subscribed to for use in driver notifications."
diff --git a/src/metabase/events/last_login.clj b/src/metabase/events/last_login.clj
index 9a857426931423faf833e15cf6f1b9cb63d5220d..aef66c07f42b08725dde9f449a4542f1844066f8 100644
--- a/src/metabase/events/last_login.clj
+++ b/src/metabase/events/last_login.clj
@@ -1,11 +1,11 @@
 (ns metabase.events.last-login
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.events :as events]
+            [metabase
+             [events :as events]
+             [util :as u]]
             [metabase.models.user :refer [User]]
-            [metabase.util :as u]))
-
+            [toucan.db :as db]))
 
 (def ^:const last-login-topics
   "The `Set` of event topics which are subscribed to for use in last login tracking."
diff --git a/src/metabase/events/metabot_lifecycle.clj b/src/metabase/events/metabot_lifecycle.clj
index e5bc8a4f5b8315bb5041ded0469fb82d08cf6c3d..e0243b22cfc674c1f6c112ebf8c76a39e3231269 100644
--- a/src/metabase/events/metabot_lifecycle.clj
+++ b/src/metabase/events/metabot_lifecycle.clj
@@ -1,12 +1,9 @@
 (ns metabase.events.metabot-lifecycle
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            [metabase.events :as events]
-            [metabase.metabot :as metabot]
-            [metabase.models.database :refer [Database]]))
-
+            [metabase
+             [events :as events]
+             [metabot :as metabot]]))
 
 (def ^:const ^:private metabot-lifecycle-topics
   "The `Set` of event topics which are subscribed to for use in metabot lifecycle."
diff --git a/src/metabase/events/notifications.clj b/src/metabase/events/notifications.clj
index 0c467912dc0ebe86a8d266e231688fbf05e2d85e..704332c689e972daf517299e6f63693603bfd2fd 100644
--- a/src/metabase/events/notifications.clj
+++ b/src/metabase/events/notifications.clj
@@ -2,19 +2,19 @@
   (:require [clojure.core.async :as async]
             [clojure.set :as set]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
             [metabase.email.messages :as messages]
             [metabase.events :as events]
-            (metabase.models [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [dashboard-card :refer [DashboardCard]]
-                             [dependency :refer [Dependency]]
-                             [metric :refer [Metric]]
-                             [pulse :refer [Pulse]]
-                             [pulse-card :refer [PulseCard]]
-                             [segment :refer [Segment]]
-                             [user :refer [User]])))
-
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [dashboard-card :refer [DashboardCard]]
+             [dependency :refer [Dependency]]
+             [metric :refer [Metric]]
+             [pulse :refer [Pulse]]
+             [pulse-card :refer [PulseCard]]
+             [segment :refer [Segment]]
+             [user :refer [User]]]
+            [toucan.db :as db]))
 
 (def ^:const notifications-topics
   "The `Set` of event topics which are subscribed to for use in notifications tracking."
diff --git a/src/metabase/events/sync_database.clj b/src/metabase/events/sync_database.clj
index 28a4ba063a2bb1941622d7be2169fadd0813e90b..7172e2fd98e8ff5f19adc2f4afdf86b72002b92e 100644
--- a/src/metabase/events/sync_database.clj
+++ b/src/metabase/events/sync_database.clj
@@ -1,11 +1,10 @@
 (ns metabase.events.sync-database
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.events :as events]
-            [metabase.models.database :refer [Database]]
-            [metabase.sync-database :as sync-database]))
-
+            [metabase
+             [events :as events]
+             [sync-database :as sync-database]]
+            [metabase.models.database :refer [Database]]))
 
 (def ^:const sync-database-topics
   "The `Set` of event topics which are subscribed to for use in database syncing."
diff --git a/src/metabase/events/view_log.clj b/src/metabase/events/view_log.clj
index a676d6ff921237cc203e1dd06513c831376451d5..b106f013297e85aa3cfa7ced413629e6e3839084 100644
--- a/src/metabase/events/view_log.clj
+++ b/src/metabase/events/view_log.clj
@@ -1,10 +1,9 @@
 (ns metabase.events.view-log
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
             [metabase.events :as events]
-            [metabase.models.view-log :refer [ViewLog]]))
-
+            [metabase.models.view-log :refer [ViewLog]]
+            [toucan.db :as db]))
 
 (def ^:private ^:const view-counts-topics
   "The `Set` of event topics which we subscribe to for view counting."
diff --git a/src/metabase/integrations/slack.clj b/src/metabase/integrations/slack.clj
index 2bcc5dc823eb681821e7a44cc0ddae629e6589f6..aa3243976a26d6c509631ee6c2567b2e822e6f2f 100644
--- a/src/metabase/integrations/slack.clj
+++ b/src/metabase/integrations/slack.clj
@@ -1,11 +1,10 @@
 (ns metabase.integrations.slack
-  (:require [clojure.tools.logging :as log]
-            [cheshire.core :as json]
+  (:require [cheshire.core :as json]
             [clj-http.client :as http]
-            [metabase.models.setting :as setting, :refer [defsetting]]
+            [clojure.tools.logging :as log]
+            [metabase.models.setting :as setting :refer [defsetting]]
             [metabase.util :as u]))
 
-
 ;; Define a setting which captures our Slack api token
 (defsetting slack-token "Slack API bearer token obtained from https://api.slack.com/web#authentication")
 
diff --git a/src/metabase/logger.clj b/src/metabase/logger.clj
index bf759429d9fbdfcb88d64cf2a13d27da8386ea56..ae397a67a12ea484e7f5a1cdea519a49bfe11233 100644
--- a/src/metabase/logger.clj
+++ b/src/metabase/logger.clj
@@ -1,12 +1,13 @@
 (ns metabase.logger
-  (:require [amalloy.ring-buffer :refer [ring-buffer]]
-            (clj-time [core :as t]
-                      [coerce :as coerce]
-                      [format :as time]))
   (:gen-class
    :extends org.apache.log4j.AppenderSkeleton
    :name metabase.logger.Appender)
-  (:import (org.apache.log4j.spi LoggingEvent)))
+  (:require [amalloy.ring-buffer :refer [ring-buffer]]
+            [clj-time
+             [coerce :as coerce]
+             [core :as t]
+             [format :as time]])
+  (:import org.apache.log4j.spi.LoggingEvent))
 
 (def ^:private ^:const max-log-entries 2500)
 
diff --git a/src/metabase/metabot.clj b/src/metabase/metabot.clj
index fc2f2fc6e417561ed20fa35ded11a28e7e7ad870..aa99175cc4989fcc26fbdf9b43a8ec3a2f49371e 100644
--- a/src/metabase/metabot.clj
+++ b/src/metabase/metabot.clj
@@ -1,26 +1,29 @@
 (ns metabase.metabot
   (:refer-clojure :exclude [list +])
-  (:require (clojure [edn :as edn]
-                     [string :as str])
+  (:require [aleph.http :as aleph]
+            [cheshire.core :as json]
+            [clojure
+             [edn :as edn]
+             [string :as str]]
             [clojure.java.io :as io]
             [clojure.tools.logging :as log]
-            [aleph.http :as aleph]
-            [cheshire.core :as json]
-            (manifold [bus :as bus]
-                      [deferred :as d]
-                      [stream :as s])
-            [throttle.core :as throttle]
+            [manifold
+             [deferred :as d]
+             [stream :as s]]
+            [metabase
+             [pulse :as pulse]
+             [util :as u]]
             [metabase.api.common :refer [*current-user-permissions-set* read-check]]
-            [toucan.db :as db]
             [metabase.integrations.slack :as slack]
-            (metabase.models [card :refer [Card]]
-                             [interface :as mi]
-                             [permissions :refer [Permissions]]
-                             [permissions-group :as perms-group]
-                             [setting :refer [defsetting], :as setting])
-            (metabase [pulse :as pulse]
-                      [util :as u])
-            [metabase.util.urls :as urls]))
+            [metabase.models
+             [card :refer [Card]]
+             [interface :as mi]
+             [permissions :refer [Permissions]]
+             [permissions-group :as perms-group]
+             [setting :as setting :refer [defsetting]]]
+            [metabase.util.urls :as urls]
+            [throttle.core :as throttle]
+            [toucan.db :as db]))
 
 (defsetting metabot-enabled
   "Enable MetaBot, which lets you search for and view your saved questions directly via Slack."
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index 88f19c572b8683105bff19f4bfbbbbf5b27c4800..a521f291aeee14404af9ccd4c6c5a8b1d2a0d07f 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -1,21 +1,23 @@
 (ns metabase.middleware
   "Metabase-specific middleware functions & configuration."
-  (:require [clojure.tools.logging :as log]
-            (cheshire factory
-                      [generate :refer [add-encoder encode-str encode-nil]])
-            monger.json ; Monger provides custom JSON encoders for Cheshire if you load this namespace -- see http://clojuremongodb.info/articles/integration.html
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.api.common :refer [*current-user* *current-user-id* *is-superuser?* *current-user-permissions-set*]]
+  (:require [cheshire.generate :refer [add-encoder encode-nil encode-str]]
+            [clojure.tools.logging :as log]
+            [metabase
+             [config :as config]
+             [db :as mdb]
+             [public-settings :as public-settings]
+             [util :as u]]
+            [metabase.api.common :refer [*current-user* *current-user-id* *current-user-permissions-set* *is-superuser?*]]
             [metabase.api.common.internal :refer [*automatically-catch-api-exceptions*]]
-            [metabase.config :as config]
             [metabase.core.initialization-status :as init-status]
-            [metabase.db :as mdb]
-            (metabase.models [session :refer [Session]]
-                             [setting :refer [defsetting]]
-                             [user :refer [User], :as user])
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u])
+            [metabase.models
+             [session :refer [Session]]
+             [setting :refer [defsetting]]
+             [user :as user :refer [User]]]
+            monger.json
+            [toucan
+             [db :as db]
+             [models :as models]])
   (:import com.fasterxml.jackson.core.JsonGenerator))
 
 ;;; # ------------------------------------------------------------ UTIL FNS ------------------------------------------------------------
diff --git a/src/metabase/models/activity.clj b/src/metabase/models/activity.clj
index 2990011226214fc926c8099eaea9c4e51618c1e2..9d62f80f3bb8539c86b17a7dbc221e4e44c0c2bd 100644
--- a/src/metabase/models/activity.clj
+++ b/src/metabase/models/activity.clj
@@ -1,15 +1,17 @@
 (ns metabase.models.activity
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.events :as events]
-            (metabase.models [card :refer [Card]]
-                             [dashboard :refer [Dashboard]]
-                             [interface :as i]
-                             [metric :refer [Metric]]
-                             [pulse :refer [Pulse]]
-                             [segment :refer [Segment]])
-            [metabase.util :as u]))
-
+  (:require [metabase
+             [events :as events]
+             [util :as u]]
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard :refer [Dashboard]]
+             [interface :as i]
+             [metric :refer [Metric]]
+             [pulse :refer [Pulse]]
+             [segment :refer [Segment]]]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;;; ------------------------------------------------------------ Perms Checking ------------------------------------------------------------
 
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index a41bcb2ff054854b0a11083fa363dafd408545ce..aa5e91a5d05a1d66b2a1787dc17d0001ab8f1d16 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -2,23 +2,24 @@
   (:require [clojure.core.memoize :as memoize]
             [clojure.tools.logging :as log]
             [medley.core :as m]
-            [metabase.api.common :refer [*current-user-id* *current-user-permissions-set*], :as api]
-            (toucan [db :as db]
-                    [models :as models])
-            (metabase.models [card-label :refer [CardLabel]]
-                             [collection :refer [Collection], :as collection]
-                             [dependency :as dependency]
-                             [interface :as i]
-                             [label :refer [Label]]
-                             [permissions :as perms]
-                             [revision :as revision]
-                             [user :as user])
-            [metabase.public-settings :as public-settings]
-            [metabase.query :as q]
-            [metabase.query-processor :as qp]
+            [metabase
+             [public-settings :as public-settings]
+             [query :as q]
+             [query-processor :as qp]
+             [util :as u]]
+            [metabase.api.common :as api :refer [*current-user-id*]]
+            [metabase.models
+             [card-label :refer [CardLabel]]
+             [collection :as collection]
+             [dependency :as dependency]
+             [interface :as i]
+             [label :refer [Label]]
+             [permissions :as perms]
+             [revision :as revision]]
             [metabase.query-processor.permissions :as qp-perms]
-            [metabase.util :as u]))
-
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel Card :report_card)
 
diff --git a/src/metabase/models/card_favorite.clj b/src/metabase/models/card_favorite.clj
index c689e13c86770224f9d9ef19cc3d698e54979c6a..2a587a56fc3320de19f75f1e12e83371a95de6b9 100644
--- a/src/metabase/models/card_favorite.clj
+++ b/src/metabase/models/card_favorite.clj
@@ -1,6 +1,6 @@
 (ns metabase.models.card-favorite
-  (:require [toucan.models :as models]
-            [metabase.util :as u]))
+  (:require [metabase.util :as u]
+            [toucan.models :as models]))
 
 (models/defmodel CardFavorite :report_cardfavorite)
 
diff --git a/src/metabase/models/card_label.clj b/src/metabase/models/card_label.clj
index b35cc24b6cdabc17d169c366e8507c00fae36c42..293c8d9ecadd86a70ea19c6c2d541530903a190a 100644
--- a/src/metabase/models/card_label.clj
+++ b/src/metabase/models/card_label.clj
@@ -1,7 +1,7 @@
 (ns ^:deprecated metabase.models.card-label
-  (:require [toucan.models :as models]
-            [metabase.models.interface :as i]
-            [metabase.util :as u]))
+  (:require [metabase.models.interface :as i]
+            [metabase.util :as u]
+            [toucan.models :as models]))
 
 (models/defmodel ^:deprecated CardLabel :card_label)
 
diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj
index f2b6541cc9014ad59d602def7b654cbf1dc7aee1..2a885d5c72361b232f6fdc61d6d85b5a82cc6a39 100644
--- a/src/metabase/models/collection.clj
+++ b/src/metabase/models/collection.clj
@@ -1,15 +1,18 @@
 (ns metabase.models.collection
-  (:require (clojure [data :as data]
-                     [string :as str])
-            [schema.core :as s]
+  (:require [clojure
+             [data :as data]
+             [string :as str]]
             [metabase.api.common :refer [*current-user-id*]]
-            (toucan [db :as db]
-                    [models :as models])
-            (metabase.models [collection-revision :refer [CollectionRevision], :as collection-revision]
-                             [interface :as i]
-                             [permissions :as perms])
+            [metabase.models
+             [collection-revision :as collection-revision :refer [CollectionRevision]]
+             [interface :as i]
+             [permissions :as perms]]
             [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (def ^:private ^:const collection-slug-max-length
   "Maximum number of characters allowed in a Collection `slug`."
diff --git a/src/metabase/models/collection_revision.clj b/src/metabase/models/collection_revision.clj
index 9b83755b8cb269cb1cbe1a68cefdcb0d8f5c3e01..c91d27974745c6647f0876e4e7f546ee2a278eba 100644
--- a/src/metabase/models/collection_revision.clj
+++ b/src/metabase/models/collection_revision.clj
@@ -1,7 +1,8 @@
 (ns metabase.models.collection-revision
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.util :as u]))
+  (:require [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel CollectionRevision :collection_revision)
 
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index 660819acfdaf2294b49759750e760996da2c48ec..4e8bb85053f9a34ec9f292209db0e2c4c845f88e 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -1,18 +1,19 @@
 (ns metabase.models.dashboard
   (:require [clojure.data :refer [diff]]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]]
-                    [models :as models])
-            [metabase.events :as events]
-            (metabase.models [card :refer [Card], :as card]
-                             [dashboard-card :refer [DashboardCard], :as dashboard-card]
-                             [interface :as i]
-                             [permissions :as perms]
-                             [revision :as revision])
+            [metabase
+             [events :as events]
+             [public-settings :as public-settings]
+             [util :as u]]
+            [metabase.models
+             [card :as card :refer [Card]]
+             [dashboard-card :as dashboard-card :refer [DashboardCard]]
+             [interface :as i]
+             [revision :as revision]]
             [metabase.models.revision.diff :refer [build-sentence]]
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u]))
-
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]
+             [models :as models]]))
 
 ;;; ---------------------------------------- Perms Checking ----------------------------------------
 
@@ -89,26 +90,6 @@
        (events/publish-event! :dashboard-create)))
 
 
-
-(defn update-dashboard!
-  "Update a `Dashboard`"
-  [dashboard user-id]
-  {:pre [(map? dashboard)
-         (u/maybe? u/sequence-of-maps? (:parameters dashboard))
-         (integer? user-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))))
-
-
 ;;; ## ---------------------------------------- REVISIONS ----------------------------------------
 
 (defn serialize-dashboard
diff --git a/src/metabase/models/dashboard_card.clj b/src/metabase/models/dashboard_card.clj
index 06fa875a08032448fd884406442eeb560a30c426..2e467589d837fd925e1a463c5f26a2bcbf6017b2 100644
--- a/src/metabase/models/dashboard_card.clj
+++ b/src/metabase/models/dashboard_card.clj
@@ -1,14 +1,17 @@
 (ns metabase.models.dashboard-card
   (:require [clojure.set :as set]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]]
-                    [models :as models])
-            [metabase.db :as mdb]
-            [metabase.events :as events]
-            (metabase.models  [card :refer [Card]]
-                              [dashboard-card-series :refer [DashboardCardSeries]]
-                              [interface :as i])
-            [metabase.util :as u]))
+            [metabase
+             [db :as mdb]
+             [events :as events]
+             [util :as u]]
+            [metabase.models
+             [card :refer [Card]]
+             [dashboard-card-series :refer [DashboardCardSeries]]
+             [interface :as i]]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]
+             [models :as models]]))
 
 (models/defmodel DashboardCard :report_dashboardcard)
 
diff --git a/src/metabase/models/dashboard_favorite.clj b/src/metabase/models/dashboard_favorite.clj
new file mode 100644
index 0000000000000000000000000000000000000000..c603e1b836167651d4c32f496ac5bdcd3d945f2a
--- /dev/null
+++ b/src/metabase/models/dashboard_favorite.clj
@@ -0,0 +1,4 @@
+(ns metabase.models.dashboard-favorite
+  (:require [toucan.models :as models]))
+
+(models/defmodel DashboardFavorite :dashboard_favorite)
diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj
index 412d6bae8ac681f2bb9375a269f2a060f8e7da0c..f84d61e3a169d6ebfdd73290ecd40117a694b5db 100644
--- a/src/metabase/models/database.clj
+++ b/src/metabase/models/database.clj
@@ -1,14 +1,16 @@
 (ns metabase.models.database
-  (:require [clojure.string :as s]
-            [cheshire.generate :refer [add-encoder encode-map]]
+  (:require [cheshire.generate :refer [add-encoder encode-map]]
+            [metabase
+             [db :as mdb]
+             [util :as u]]
             [metabase.api.common :refer [*current-user*]]
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.db :as mdb]
-            (metabase.models [interface :as i]
-                             [permissions, :as perms]
-                             [permissions-group :as perm-group])
-            [metabase.util :as u]))
+            [metabase.models
+             [interface :as i]
+             [permissions :as perms]
+             [permissions-group :as perm-group]]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;;; ------------------------------------------------------------ Entity & Lifecycle ------------------------------------------------------------
 
diff --git a/src/metabase/models/dependency.clj b/src/metabase/models/dependency.clj
index 943090174e680510f439a64d5db95e2cfb51cf0d..529ca83e26e1bd3c0f2f4a13656ff65391d5ddc1 100644
--- a/src/metabase/models/dependency.clj
+++ b/src/metabase/models/dependency.clj
@@ -1,8 +1,9 @@
 (ns metabase.models.dependency
   (:require [clojure.set :as set]
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.util :as u]))
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (defprotocol IDependent
   "Methods an entity may optionally implement to control how dependencies of an instance are captured."
diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj
index aa75669e28d5fd0f63b9df0c57f9ddf77e4a5b7c..1ff8fee88f1049ed8351cc402ec8734e2a0dec79 100644
--- a/src/metabase/models/field.clj
+++ b/src/metabase/models/field.clj
@@ -1,16 +1,18 @@
 (ns metabase.models.field
-  (:require (clojure [data :as d]
-                     [string :as s])
-            [metabase.config :as config]
-            (toucan [db :as db]
-                    [models :as models])
-            metabase.types
-            (metabase.models [field-values :refer [FieldValues]]
-                             [humanization :as humanization]
-                             [interface :as i]
-                             [permissions :as perms])
-            [metabase.util :as u]))
-
+  (:require [clojure
+             [data :as d]
+             [string :as s]]
+            [metabase
+             [config :as config]
+             [util :as u]]
+            [metabase.models
+             [field-values :refer [FieldValues]]
+             [humanization :as humanization]
+             [interface :as i]
+             [permissions :as perms]]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;;; ------------------------------------------------------------ Type Mappings ------------------------------------------------------------
 
diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj
index 2a8fcf8751e8d5c86c53eb517127e9ad2d35d6fd..2c64fcd33d67d5316160c26decc109dcb8ce15c5 100644
--- a/src/metabase/models/field_values.clj
+++ b/src/metabase/models/field_values.clj
@@ -1,8 +1,9 @@
 (ns metabase.models.field-values
   (:require [clojure.tools.logging :as log]
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.util :as u]))
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;; ## Entity + DB Multimethods
 
diff --git a/src/metabase/models/humanization.clj b/src/metabase/models/humanization.clj
index 0878761bf883ac73bea68073a9cd14282847bad7..e0d41018db11c0b91e56ffbbf3e3562ef6c8d0a2 100644
--- a/src/metabase/models/humanization.clj
+++ b/src/metabase/models/humanization.clj
@@ -8,9 +8,9 @@
    The actual algorithm for advanced humanization is in `metabase.util.infer-spaces`."
   (:require [clojure.string :as s]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.models.setting :refer [defsetting], :as setting]
-            [metabase.util.infer-spaces :refer [infer-spaces]]))
+            [metabase.models.setting :as setting :refer [defsetting]]
+            [metabase.util.infer-spaces :refer [infer-spaces]]
+            [toucan.db :as db]))
 
 (def ^:private ^:const acronyms
   #{"id" "url" "ip" "uid" "uuid" "guid"})
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 308210288029306ae3d095a390eb1a00a84bfffd..c7c2ace99bdf05c96530b78bfa3dd98537870b7b 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -1,11 +1,10 @@
 (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]
+  (:require [cheshire.core :as json]
+            [clojure.core.memoize :as memoize]
             [metabase.util :as u]
-            [metabase.util.encryption :as encryption])
+            [metabase.util.encryption :as encryption]
+            [taoensso.nippy :as nippy]
+            [toucan.models :as models])
   (:import java.sql.Blob))
 
 ;;; ------------------------------------------------------------ Toucan Extensions ------------------------------------------------------------
diff --git a/src/metabase/models/label.clj b/src/metabase/models/label.clj
index db8c6d6252d0f094f3da674bafa1b2722813c91e..fd364a3eb26734aa6738d063d54df83e6a3dde87 100644
--- a/src/metabase/models/label.clj
+++ b/src/metabase/models/label.clj
@@ -1,9 +1,10 @@
 (ns ^:deprecated metabase.models.label
   "Labels that can be applied to Cards. Deprecated in favor of Collections."
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.models.interface :as i]
-            [metabase.util :as u]))
+  (:require [metabase.models.interface :as i]
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel ^:deprecated Label :label)
 
diff --git a/src/metabase/models/metric.clj b/src/metabase/models/metric.clj
index 5f91e2e7fc9e637f4a02c3dda40fadabb4c47c1d..81fb9cf81b1e22e5c2fb72d95affa7342437395c 100644
--- a/src/metabase/models/metric.clj
+++ b/src/metabase/models/metric.clj
@@ -1,15 +1,17 @@
 (ns metabase.models.metric
   (:require [medley.core :as m]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]]
-                    [models :as models])
-            [metabase.events :as events]
-            (metabase.models [dependency :as dependency]
-                             [interface :as i]
-                             [revision :as revision])
-            [metabase.query :as q]
-            [metabase.util :as u]))
-
+            [metabase
+             [events :as events]
+             [query :as q]
+             [util :as u]]
+            [metabase.models
+             [dependency :as dependency]
+             [interface :as i]
+             [revision :as revision]]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]
+             [models :as models]]))
 
 (models/defmodel Metric :metric)
 
@@ -49,8 +51,8 @@
                                                (select-keys metric1 [:name :description :definition])
                                                (select-keys metric2 [:name :description :definition]))]
       (cond-> (merge-with merge
-                          (m/map-vals (fn [v] {:after v}) (:after base-diff))
-                          (m/map-vals (fn [v] {:before v}) (:before base-diff)))
+                (m/map-vals (fn [v] {:after v}) (:after base-diff))
+                (m/map-vals (fn [v] {:before v}) (:before base-diff)))
         (or (get-in base-diff [:after :definition])
             (get-in base-diff [:before :definition])) (assoc :definition {:before (get-in metric1 [:definition])
                                                                           :after  (get-in metric2 [:definition])})))))
@@ -117,16 +119,20 @@
    (retrieve-metrics table-id :active))
   ([table-id state]
    {:pre [(integer? table-id) (keyword? state)]}
-   (-> (if (= :all state)
-         (db/select Metric, :table_id table-id, {:order-by [[:name :asc]]})
-         (db/select Metric, :table_id table-id, :is_active (= :active state), {:order-by [[:name :asc]]}))
+   (-> (db/select Metric
+         {:where    [:and [:= :table_id table-id]
+                          (case state
+                            :all     true
+                            :active  [:= :is_active true]
+                            :deleted [:= :is_active false])]
+          :order-by [[:name :asc]]})
        (hydrate :creator))))
 
 (defn update-metric!
   "Update an existing `Metric`.
 
    Returns the updated `Metric` or throws an Exception."
-  [{:keys [id name description caveats points_of_interest how_is_this_calculated show_in_getting_started definition revision_message]} user-id]
+  [{:keys [id name definition revision_message], :as body} user-id]
   {:pre [(integer? id)
          (string? name)
          (map? definition)
@@ -134,13 +140,7 @@
          (string? revision_message)]}
   ;; update the metric itself
   (db/update! Metric id
-    :name                    name
-    :description             description
-    :caveats                 caveats
-    :points_of_interest      points_of_interest
-    :how_is_this_calculated  how_is_this_calculated
-    :show_in_getting_started show_in_getting_started
-    :definition              definition)
+    (select-keys body #{:caveats :definition :description :how_is_this_calculated :name :points_of_interest :show_in_getting_started}))
   (u/prog1 (retrieve-metric id)
     (events/publish-event! :metric-update (assoc <> :actor_id user-id, :revision_message revision_message))))
 
diff --git a/src/metabase/models/metric_important_field.clj b/src/metabase/models/metric_important_field.clj
index dff23d7f3b2358a1dfa05a30cce250f39a7b2a5b..cf63488f7c387ec4e9abc8ac95b1e63aa25c69c8 100644
--- a/src/metabase/models/metric_important_field.clj
+++ b/src/metabase/models/metric_important_field.clj
@@ -1,8 +1,8 @@
 (ns metabase.models.metric-important-field
   "Intersection table for `Metric` and `Field`; this is used to keep track of the top 0-3 important fields for a metric as shown in the Getting Started guide."
-  (:require [toucan.models :as models]
-            [metabase.models.interface :as i]
-            [metabase.util :as u]))
+  (:require [metabase.models.interface :as i]
+            [metabase.util :as u]
+            [toucan.models :as models]))
 
 (models/defmodel MetricImportantField :metric_important_field)
 
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index 09bd880f1a7f7b91e47465a0112e0615c16c1a62..1f1523aa0bf9dc6f28274e654b8ed714841d2238 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -1,19 +1,22 @@
 (ns metabase.models.permissions
-  (:require [clojure.core.match :refer [match]]
-            [clojure.data :as data]
-            [clojure.string :as str]
+  (:require [clojure
+             [data :as data]
+             [string :as str]]
+            [clojure.core.match :refer [match]]
             [clojure.tools.logging :as log]
             [medley.core :as m]
-            [schema.core :as s]
-            (toucan [db :as db]
-                    [models :as models])
             [metabase.api.common :refer [*current-user-id*]]
-            (metabase.models [permissions-group :as group]
-                             [permissions-revision :refer [PermissionsRevision] :as perms-revision])
+            [metabase.models
+             [permissions-group :as group]
+             [permissions-revision :as perms-revision :refer [PermissionsRevision]]]
             [metabase.util :as u]
-            (metabase.util [honeysql-extensions :as hx]
-                           [schema :as su])))
-
+            [metabase.util
+             [honeysql-extensions :as hx]
+             [schema :as su]]
+            [schema.core :as s]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;;; +------------------------------------------------------------------------------------------------------------------------------------------------------+
 ;;; |                                                                      UTIL FNS                                                                        |
diff --git a/src/metabase/models/permissions_group.clj b/src/metabase/models/permissions_group.clj
index 6d966baa27295a2c5057b2373453dcdd92e829dc..b3e04e75d68eb6f46c77ad0c5d3cb370182ebf11 100644
--- a/src/metabase/models/permissions_group.clj
+++ b/src/metabase/models/permissions_group.clj
@@ -1,9 +1,10 @@
 (ns metabase.models.permissions-group
-  (:require [clojure.tools.logging :as log]
-            [clojure.string :as s]
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.util :as u]))
+  (:require [clojure.string :as s]
+            [clojure.tools.logging :as log]
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel PermissionsGroup :permissions_group)
 
diff --git a/src/metabase/models/permissions_group_membership.clj b/src/metabase/models/permissions_group_membership.clj
index 3d9f67b2eef0ee547f53148f62958839c2d4c675..2058206bc250fff63dc610bf0db16191fec49bd9 100644
--- a/src/metabase/models/permissions_group_membership.clj
+++ b/src/metabase/models/permissions_group_membership.clj
@@ -1,8 +1,9 @@
 (ns metabase.models.permissions-group-membership
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.models.permissions-group :as group]
-            [metabase.util :as u]))
+  (:require [metabase.models.permissions-group :as group]
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel PermissionsGroupMembership :permissions_group_membership)
 
diff --git a/src/metabase/models/permissions_revision.clj b/src/metabase/models/permissions_revision.clj
index 9a8dbc6615fb0d33bafebdfe633baeb84e681ed3..9d14c93f0d8afd89bd020c65ead9e4d4ede0526b 100644
--- a/src/metabase/models/permissions_revision.clj
+++ b/src/metabase/models/permissions_revision.clj
@@ -1,7 +1,8 @@
 (ns metabase.models.permissions-revision
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.util :as u]))
+  (:require [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel PermissionsRevision :permissions_revision)
 
diff --git a/src/metabase/models/pulse.clj b/src/metabase/models/pulse.clj
index 7993cd7b01c078e025da215fa932edc27a631f8f..f9517962067f1ed83540e0faa197d12277a2f8ab 100644
--- a/src/metabase/models/pulse.clj
+++ b/src/metabase/models/pulse.clj
@@ -1,17 +1,20 @@
 (ns metabase.models.pulse
   (:require [clojure.set :as set]
             [medley.core :as m]
+            [metabase
+             [db :as mdb]
+             [events :as events]
+             [util :as u]]
             [metabase.api.common :refer [*current-user*]]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]]
-                    [models :as models])
-            [metabase.db :as mdb]
-            [metabase.events :as events]
-            (metabase.models [card :refer [Card]]
-                             [interface :as i]
-                             [pulse-card :refer [PulseCard]]
-                             [pulse-channel :refer [PulseChannel] :as pulse-channel])
-            [metabase.util :as u]))
+            [metabase.models
+             [card :refer [Card]]
+             [interface :as i]
+             [pulse-card :refer [PulseCard]]
+             [pulse-channel :as pulse-channel :refer [PulseChannel]]]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]
+             [models :as models]]))
 
 ;;; ------------------------------------------------------------ Perms Checking ------------------------------------------------------------
 
diff --git a/src/metabase/models/pulse_channel.clj b/src/metabase/models/pulse_channel.clj
index e7b2fddaecabfb06293c5a004a7afd4f5667c6c7..3dd4ec09e24c6bee9936193b838024818febb579 100644
--- a/src/metabase/models/pulse_channel.clj
+++ b/src/metabase/models/pulse_channel.clj
@@ -1,14 +1,15 @@
 (ns metabase.models.pulse-channel
-  (:require [clojure.set :as set]
-            [cheshire.generate :refer [add-encoder encode-map]]
+  (:require [cheshire.generate :refer [add-encoder encode-map]]
+            [clojure.set :as set]
             [medley.core :as m]
-            (toucan [db :as db]
-                    [models :as models])
-            (metabase.models [pulse-channel-recipient :refer [PulseChannelRecipient]]
-                             [interface :as i]
-                             [user :refer [User]])
-            [metabase.util :as u]))
-
+            [metabase.models
+             [interface :as i]
+             [pulse-channel-recipient :refer [PulseChannelRecipient]]
+             [user :refer [User]]]
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;; ## Static Definitions
 
diff --git a/src/metabase/models/query.clj b/src/metabase/models/query.clj
index 74c80cba809607118cb40df8ac24b4ee3354e5be..35eb4b74e5ddc5d6618e0968ecca7763566fce56 100644
--- a/src/metabase/models/query.clj
+++ b/src/metabase/models/query.clj
@@ -1,9 +1,9 @@
 (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]))
+  (:require [metabase.db :as mdb]
+            [metabase.util.honeysql-extensions :as hx]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel Query :query)
 
diff --git a/src/metabase/models/query_cache.clj b/src/metabase/models/query_cache.clj
index e4277919d641cb58629f75311f2da3093c13794a..7b1a974430a959cb8b145dbd3fa5fbb44be9af66 100644
--- a/src/metabase/models/query_cache.clj
+++ b/src/metabase/models/query_cache.clj
@@ -1,7 +1,7 @@
 (ns metabase.models.query-cache
   "A model used to cache query results in the database."
-  (:require [toucan.models :as models]
-            [metabase.util :as u]))
+  (:require [metabase.util :as u]
+            [toucan.models :as models]))
 
 (models/defmodel QueryCache :query_cache)
 
diff --git a/src/metabase/models/query_execution.clj b/src/metabase/models/query_execution.clj
index 83e7ea1597cd4b148a50d5fdfa45a8d4bc4cfdfa..eb71e22c9cbd5c4df04f71dbd5305256ee7cc0fe 100644
--- a/src/metabase/models/query_execution.clj
+++ b/src/metabase/models/query_execution.clj
@@ -1,8 +1,7 @@
 (ns metabase.models.query-execution
-  (:require [schema.core :as s]
-            [toucan.models :as models]
-            [metabase.util :as u]))
-
+  (:require [metabase.util :as u]
+            [schema.core :as s]
+            [toucan.models :as models]))
 
 (models/defmodel QueryExecution :query_execution)
 
diff --git a/src/metabase/models/raw_column.clj b/src/metabase/models/raw_column.clj
index 20e15fcfe279a4162a3165872775800b6a517a07..7a9e50eaf190e6af9528d51318462d50659eb757 100644
--- a/src/metabase/models/raw_column.clj
+++ b/src/metabase/models/raw_column.clj
@@ -1,8 +1,8 @@
 (ns metabase.models.raw-column
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.util :as u]))
-
+  (:require [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel RawColumn :raw_column)
 
diff --git a/src/metabase/models/raw_table.clj b/src/metabase/models/raw_table.clj
index eda32cdfa265e88c5321fa1e345ff35b3bc4442e..b462de633dd62910ac88b993ebcd5074ca92d997 100644
--- a/src/metabase/models/raw_table.clj
+++ b/src/metabase/models/raw_table.clj
@@ -1,9 +1,9 @@
 (ns metabase.models.raw-table
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.models.raw-column :refer [RawColumn]]
-            [metabase.util :as u]))
-
+  (:require [metabase.models.raw-column :refer [RawColumn]]
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel RawTable :raw_table)
 
diff --git a/src/metabase/models/revision.clj b/src/metabase/models/revision.clj
index e6ee7b44a5a8e97b50a71073d202fa97d7c35f4a..baa99c179ede0602786a2d91d8f33b84942b94ba 100644
--- a/src/metabase/models/revision.clj
+++ b/src/metabase/models/revision.clj
@@ -1,11 +1,12 @@
 (ns metabase.models.revision
   (:require [clojure.data :as data]
-            (toucan [db :as db]
-                    [models :as models]
-                    [hydrate :refer [hydrate]])
-            [metabase.models.user :refer [User]]
             [metabase.models.revision.diff :refer [diff-string]]
-            [metabase.util :as u]))
+            [metabase.models.user :refer [User]]
+            [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]
+             [models :as models]]))
 
 (def ^:const max-revisions
   "Maximum number of revisions to keep for each individual object. After this limit is surpassed, the oldest revisions will be deleted."
diff --git a/src/metabase/models/segment.clj b/src/metabase/models/segment.clj
index 1236d306228108af32959faed351df90db6c729c..5942f8b3824acf84fc34055252a5be306fbfa3bf 100644
--- a/src/metabase/models/segment.clj
+++ b/src/metabase/models/segment.clj
@@ -1,13 +1,15 @@
 (ns metabase.models.segment
   (:require [medley.core :as m]
-            (toucan [db :as db]
-                    [hydrate :refer [hydrate]]
-                    [models :as models])
-            [metabase.events :as events]
-            (metabase.models [interface :as i]
-                             [revision :as revision])
-            [metabase.util :as u]))
-
+            [metabase
+             [events :as events]
+             [util :as u]]
+            [metabase.models
+             [interface :as i]
+             [revision :as revision]]
+            [toucan
+             [db :as db]
+             [hydrate :refer [hydrate]]
+             [models :as models]]))
 
 (models/defmodel Segment :segment)
 
@@ -107,8 +109,7 @@
 (defn update-segment!
   "Update an existing `Segment`.
    Returns the updated `Segment` or throws an Exception."
-  {:style/indent 0}
-  [{:keys [id name description caveats points_of_interest show_in_getting_started definition revision_message]} user-id]
+  [{:keys [id name description caveats points_of_interest show_in_getting_started definition revision_message], :as body} user-id]
   {:pre [(integer? id)
          (string? name)
          (map? definition)
@@ -116,15 +117,9 @@
          (string? revision_message)]}
   ;; update the segment itself
   (db/update! Segment id
-    (merge
-     {:name        name
-      :description description
-      :caveats     caveats
-      :definition  definition}
-     (when (seq points_of_interest)
-       {:points_of_interest points_of_interest})
-     (when (not (nil? show_in_getting_started))
-       {:show_in_getting_started show_in_getting_started})))
+    (u/select-keys-when body
+      :present #{:name :description :caveats :definition}
+      :non-nil #{:points_of_interest :show_in_getting_started}))
   (u/prog1 (retrieve-segment id)
     (events/publish-event! :segment-update (assoc <> :actor_id user-id, :revision_message revision_message))))
 
diff --git a/src/metabase/models/session.clj b/src/metabase/models/session.clj
index bed5dfd2bf9339f2f753f6c58a578cd44018bb8a..7156847ce9463292b90f26d81d03f9038903c6aa 100644
--- a/src/metabase/models/session.clj
+++ b/src/metabase/models/session.clj
@@ -1,8 +1,8 @@
 (ns metabase.models.session
-  (:require (toucan [db :as db]
-                    [models :as models])
-            [metabase.models.user :refer [User]]
-            [metabase.util :as u]))
+  (:require [metabase.util :as u]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel Session :core_session)
 
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 713a2b92f49c6ae73399e1fd6b0d55835e7aaaca..27a05177548c6e3f93faceaef89e99a7dd175c09 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -30,17 +30,17 @@
 
       (setting/all)"
   (:refer-clojure :exclude [get])
-  (:require [clojure.string :as str]
+  (:require [cheshire.core :as json]
+            [clojure.string :as str]
             [clojure.tools.logging :as log]
-            [cheshire.core :as json]
             [environ.core :as env]
-            [medley.core :as m]
+            [metabase
+             [events :as events]
+             [util :as u]]
             [schema.core :as s]
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.events :as events]
-            [metabase.util :as u]))
-
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 (models/defmodel Setting
   "The model that underlies `defsetting`."
diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj
index bb291b2982310b98a20803efb2463b324c077fec..afebba6ca0ff40ed7c1d8473582d03fc1b3e9aa0 100644
--- a/src/metabase/models/table.clj
+++ b/src/metabase/models/table.clj
@@ -1,18 +1,19 @@
 (ns metabase.models.table
-  (:require [metabase.api.common :refer [*current-user-permissions-set*]]
-            (toucan [db :as db]
-                    [models :as models])
-            [metabase.db :as mdb]
-            (metabase.models [database :refer [Database]]
-                             [field :refer [Field]]
-                             [field-values :refer [FieldValues]]
-                             [humanization :as humanization]
-                             [interface :as i]
-                             [metric :refer [Metric retrieve-metrics]]
-                             [permissions :refer [Permissions], :as perms]
-                             [segment :refer [Segment retrieve-segments]])
-            [metabase.util :as u]))
-
+  (:require [metabase
+             [db :as mdb]
+             [util :as u]]
+            [metabase.models
+             [database :refer [Database]]
+             [field :refer [Field]]
+             [field-values :refer [FieldValues]]
+             [humanization :as humanization]
+             [interface :as i]
+             [metric :refer [Metric retrieve-metrics]]
+             [permissions :as perms :refer [Permissions]]
+             [segment :refer [retrieve-segments Segment]]]
+            [toucan
+             [db :as db]
+             [models :as models]]))
 
 ;;; ------------------------------------------------------------ Constants + Entity ------------------------------------------------------------
 
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index be7042e2c81f1953c27f45cbfa45c9dff0d4ac21..2c5b6dfa36f5c3e0b6d059a036920132798cdac4 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -1,15 +1,16 @@
 (ns metabase.models.user
-  (:require [clojure.string :as s]
-            [clojure.tools.logging :as log]
-            [cemerick.friend.credentials :as creds]
-            (toucan [db :as db]
-                    [models :as models])
+  (:require [cemerick.friend.credentials :as creds]
+            [clojure.string :as s]
+            [metabase
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.email.messages :as email]
-            (metabase.models [permissions :as perms]
-                             [permissions-group :as group]
-                             [permissions-group-membership :refer [PermissionsGroupMembership], :as perm-membership])
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u])
+            [metabase.models
+             [permissions-group :as group]
+             [permissions-group-membership :as perm-membership :refer [PermissionsGroupMembership]]]
+            [toucan
+             [db :as db]
+             [models :as models]])
   (:import java.util.UUID))
 
 ;;; ------------------------------------------------------------ Entity & Lifecycle ------------------------------------------------------------
diff --git a/src/metabase/models/view_log.clj b/src/metabase/models/view_log.clj
index c69b9699e88b6c9e3692f208fd301de983d451d0..b69b78d86f05a8c0fbc17cea1224caf597b884ef 100644
--- a/src/metabase/models/view_log.clj
+++ b/src/metabase/models/view_log.clj
@@ -1,8 +1,7 @@
 (ns metabase.models.view-log
-  (:require [toucan.models :as models]
-            [metabase.models.interface :as i]
-            [metabase.util :as u]))
-
+  (:require [metabase.models.interface :as i]
+            [metabase.util :as u]
+            [toucan.models :as models]))
 
 (models/defmodel ViewLog :view_log)
 
diff --git a/src/metabase/plugins.clj b/src/metabase/plugins.clj
index 08974e59553bfc75c248b5c3db03738cbc301752..18d3ffbecea9780b003bad19d7de051076f256bb 100644
--- a/src/metabase/plugins.clj
+++ b/src/metabase/plugins.clj
@@ -1,9 +1,10 @@
 (ns metabase.plugins
   (:require [clojure.java.io :as io]
             [clojure.tools.logging :as log]
-            [metabase.config :as config]
-            [metabase.util :as u])
-  (:import (java.net URL URLClassLoader)))
+            [metabase
+             [config :as config]
+             [util :as u]])
+  (:import [java.net URL URLClassLoader]))
 
 (defn- plugins-dir
   "The Metabase plugins directory. This defaults to `plugins/` in the same directory as `metabase.jar`, but can be configured via the env var `MB_PLUGINS_DIR`."
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 4d9900614fa575d603f9838e7abeefbc22074568..98e5d3ea17a3b5f5b1ed856f0e25039569956973 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -1,11 +1,13 @@
 (ns metabase.public-settings
   (:require [clojure.string :as s]
-            [toucan.db :as db]
-            [metabase.config :as config]
-            (metabase.models [common :as common]
-                             [setting :refer [defsetting], :as setting])
-            [metabase.types :as types]
-            [metabase.util.password :as password])
+            [metabase
+             [config :as config]
+             [types :as types]]
+            [metabase.models
+             [common :as common]
+             [setting :as setting :refer [defsetting]]]
+            [metabase.util.password :as password]
+            [toucan.db :as db])
   (:import java.util.TimeZone))
 
 (defsetting check-for-updates
diff --git a/src/metabase/pulse.clj b/src/metabase/pulse.clj
index 2a1b81b901430bd397af93f019551c1daccdaefc..d0a8919184826d4e364beed9707647685cf5ee41 100644
--- a/src/metabase/pulse.clj
+++ b/src/metabase/pulse.clj
@@ -1,19 +1,16 @@
 (ns metabase.pulse
   "Public API for sending Pulses."
-  (:require (clojure [pprint :refer [cl-format]]
-                     [string :refer [upper-case]])
-            [clojure.tools.logging :as log]
-            [metabase.email :as email]
+  (:require [clojure.tools.logging :as log]
+            [metabase
+             [email :as email]
+             [query-processor :as qp]
+             [util :as u]]
             [metabase.email.messages :as messages]
             [metabase.integrations.slack :as slack]
             [metabase.models.card :refer [Card]]
             [metabase.pulse.render :as render]
-            [metabase.query-processor :as qp]
-            [metabase.util.urls :as urls]
-            [metabase.util :as u]
             [metabase.util.urls :as urls]))
 
-
 ;;; ## ---------------------------------------- PULSE SENDING ----------------------------------------
 
 
diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index 2761f09061cf20c84c33b73e1ea90e885536b715..aeda60eff850892edae154ced2919ea14e104df0 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -1,24 +1,26 @@
 (ns metabase.pulse.render
-  (:require [clojure.java.io :as io]
-            (clojure [pprint :refer [cl-format]]
-                     [string :as s])
+  (:require [clj-time
+             [coerce :as c]
+             [core :as t]
+             [format :as f]]
+            [clojure
+             [pprint :refer [cl-format]]
+             [string :as s]]
+            [clojure.java.io :as io]
             [clojure.tools.logging :as log]
-            (clj-time [coerce :as c]
-                      [core :as t]
-                      [format :as f])
-            [hiccup.core :refer [html h]]
-            [metabase.util.urls :as urls]
-            [metabase.util :as u])
-  (:import (java.awt BasicStroke Color Dimension RenderingHints)
+            [hiccup.core :refer [h html]]
+            [metabase.util :as u]
+            [metabase.util.urls :as urls])
+  (:import cz.vutbr.web.css.MediaSpec
+           [java.awt BasicStroke Color Dimension RenderingHints]
            java.awt.image.BufferedImage
-           (java.io ByteArrayInputStream ByteArrayOutputStream)
+           [java.io ByteArrayInputStream ByteArrayOutputStream]
            java.nio.charset.StandardCharsets
            java.util.Date
            javax.imageio.ImageIO
-           cz.vutbr.web.css.MediaSpec
            org.apache.commons.io.IOUtils
-           (org.fit.cssbox.css CSSNorm DOMAnalyzer DOMAnalyzer$Origin)
-           (org.fit.cssbox.io DefaultDOMSource StreamDocumentSource)
+           [org.fit.cssbox.css CSSNorm DOMAnalyzer DOMAnalyzer$Origin]
+           [org.fit.cssbox.io DefaultDOMSource StreamDocumentSource]
            org.fit.cssbox.layout.BrowserCanvas
            org.fit.cssbox.misc.Base64Coder))
 
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index 834d312848664a35d1e38417a565cc2ed27cb2cf..57090dcebb3c307a3c184c658b214a17f9b92eba 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -1,32 +1,35 @@
 (ns metabase.query-processor
   "Preprocessor that does simple transformations to all incoming queries, simplifing the driver-specific implementations."
   (:require [clojure.tools.logging :as log]
-            [schema.core :as s]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            (metabase.models [query :as query]
-                             [query-execution :refer [QueryExecution], :as query-execution])
+            [metabase
+             [driver :as driver]
+             [util :as u]]
+            [metabase.models
+             [query :as query]
+             [query-execution :as query-execution :refer [QueryExecution]]]
+            [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]
+             [cache :as cache]
+             [catch-exceptions :as catch-exceptions]
+             [cumulative-aggregations :as cumulative-ags]
+             [dev :as dev]
+             [driver-specific :as driver-specific]
+             [expand-macros :as expand-macros]
+             [expand-resolve :as expand-resolve]
+             [format-rows :as format-rows]
+             [limit :as limit]
+             [log :as log-query]
+             [mbql-to-native :as mbql-to-native]
+             [parameters :as parameters]
+             [permissions :as perms]
+             [resolve-driver :as resolve-driver]]
             [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]
-                                                 [expand-macros :as expand-macros]
-                                                 [expand-resolve :as expand-resolve]
-                                                 [format-rows :as format-rows]
-                                                 [limit :as limit]
-                                                 [log :as log-query]
-                                                 [mbql-to-native :as mbql-to-native]
-                                                 [parameters :as parameters]
-                                                 [permissions :as perms]
-                                                 [resolve-driver :as resolve-driver])
-            [metabase.util :as u]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 ;;; +-------------------------------------------------------------------------------------------------------+
 ;;; |                                           QUERY PROCESSOR                                             |
diff --git a/src/metabase/query_processor/annotate.clj b/src/metabase/query_processor/annotate.clj
index c2a58e38195c2d32896e9f23f816b5ea63abb45a..8200086f5f84f5c2c9945c3748d3cadc4d79765b 100644
--- a/src/metabase/query_processor/annotate.clj
+++ b/src/metabase/query_processor/annotate.clj
@@ -1,18 +1,20 @@
 (ns metabase.query-processor.annotate
   "Code that analyzes the results of running a query and adds relevant type information about results (including foreign key information).
    TODO - The code in this namespace could definitely use a little cleanup to make it a little easier to wrap one's head around :)"
-  (:require [clojure.set :as set]
-            [clojure.string :as str]
+  (:require [clojure
+             [set :as set]
+             [string :as str]]
             [clojure.tools.logging :as log]
             [medley.core :as m]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.models.field :refer [Field]]
-            (metabase.query-processor [interface :as i]
-                                      [sort :as sort])
-            [metabase.util :as u])
-  (:import (metabase.query_processor.interface Expression
-                                               ExpressionRef)))
+            [metabase.query-processor
+             [interface :as i]
+             [sort :as sort]]
+            [toucan.db :as db])
+  (:import [metabase.query_processor.interface Expression ExpressionRef]))
 
 ;;; ## Field Resolution
 
diff --git a/src/metabase/query_processor/expand.clj b/src/metabase/query_processor/expand.clj
index 23f660f02db5240fca2b0d47853ddb76de327e35..a6a1d3c7bc8db4c8efebcfa0dd683ba1ab1b6191 100644
--- a/src/metabase/query_processor/expand.clj
+++ b/src/metabase/query_processor/expand.clj
@@ -2,28 +2,15 @@
   "Converts a Query Dict as received by the API into an *expanded* one that contains extra information that will be needed to
    construct the appropriate native Query, and perform various post-processing steps such as Field ordering."
   (:refer-clojure :exclude [< <= > >= = != and or not filter count distinct sum min max + - / *])
-  (:require (clojure [core :as core]
-                     [string :as str])
+  (:require [clojure
+             [core :as core]
+             [string :as str]]
             [clojure.tools.logging :as log]
-            [schema.core :as s]
-            [toucan.db :as db]
-            [metabase.models.table :refer [Table]]
             [metabase.query-processor.interface :as i]
             [metabase.util :as u]
-            [metabase.util.schema :as su])
-  (:import (metabase.query_processor.interface AgFieldRef
-                                               BetweenFilter
-                                               ComparisonFilter
-                                               CompoundFilter
-                                               EqualityFilter
-                                               Expression
-                                               ExpressionRef
-                                               FieldPlaceholder
-                                               NotFilter
-                                               RelativeDatetime
-                                               StringFilter
-                                               ValuePlaceholder)))
-
+            [metabase.util.schema :as su]
+            [schema.core :as s])
+  (:import [metabase.query_processor.interface AgFieldRef BetweenFilter ComparisonFilter CompoundFilter Expression ExpressionRef FieldPlaceholder RelativeDatetime StringFilter ValuePlaceholder]))
 
 ;;; # ------------------------------------------------------------ Token dispatch ------------------------------------------------------------
 
diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj
index cf0b81dc8b220fb8530f3aa9f28d0e496fd9384b..05397e945ad9f5d7f28868ee2469e99540cca81a 100644
--- a/src/metabase/query_processor/interface.clj
+++ b/src/metabase/query_processor/interface.clj
@@ -2,14 +2,13 @@
   "Definitions of `Field`, `Value`, and other record types present in an expanded query.
    This namespace should just contain definitions of various protocols and record types; associated logic
    should go in `metabase.query-processor.expand`."
-  (:require [schema.core :as s]
-            [metabase.models.field :as field]
+  (:require [metabase.models.field :as field]
             [metabase.util :as u]
-            [metabase.util.schema :as su])
+            [metabase.util.schema :as su]
+            [schema.core :as s])
   (:import clojure.lang.Keyword
            java.sql.Timestamp))
 
-
 ;;; # ------------------------------------------------------------ CONSTANTS ------------------------------------------------------------
 
 (def ^:const absolute-max-results
diff --git a/src/metabase/query_processor/macros.clj b/src/metabase/query_processor/macros.clj
index fab3624cfcf9cc0f6d46fd2e7d11c25ed0410a08..225c761610f5ea1e2ab567b525ba71a5098dab70 100644
--- a/src/metabase/query_processor/macros.clj
+++ b/src/metabase/query_processor/macros.clj
@@ -3,8 +3,7 @@
    At some point this ought to be reworked to be case-insensitive and cleaned up."
   (:require [clojure.core.match :refer [match]]
             [clojure.walk :as walk]
-            [toucan.db :as db]
-            [metabase.util :as u]))
+            [toucan.db :as db]))
 
 (defn- non-empty-clause? [clause]
   (and clause
diff --git a/src/metabase/query_processor/middleware/add_implicit_clauses.clj b/src/metabase/query_processor/middleware/add_implicit_clauses.clj
index 1fdb3f10a3f4ae82df854662d8b6d530690274f3..705a461061f3224a9bf9bb770ea26ed6337fe64e 100644
--- a/src/metabase/query_processor/middleware/add_implicit_clauses.clj
+++ b/src/metabase/query_processor/middleware/add_implicit_clauses.clj
@@ -1,12 +1,12 @@
 (ns metabase.query-processor.middleware.add-implicit-clauses
   "Middlware for adding an implicit `:fields` and `:order-by` clauses to certain queries."
   (:require [clojure.tools.logging :as log]
-            [toucan.db :as db]
             [metabase.models.field :refer [Field]]
-            (metabase.query-processor [interface :as i]
-                                      [resolve :as resolve]
-                                      [util :as qputil])))
-
+            [metabase.query-processor
+             [interface :as i]
+             [resolve :as resolve]
+             [util :as qputil]]
+            [toucan.db :as db]))
 
 (defn- fields-for-source-table
   "Return the all fields for SOURCE-TABLE, for use as an implicit `:fields` clause."
diff --git a/src/metabase/query_processor/middleware/cache.clj b/src/metabase/query_processor/middleware/cache.clj
index 06bea49173d50e81db33bda3737b1238e842219a..de1c58bc69278932f4a20aa8ee413498c0ee8940 100644
--- a/src/metabase/query_processor/middleware/cache.clj
+++ b/src/metabase/query_processor/middleware/cache.clj
@@ -13,11 +13,12 @@
 
     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
+             [config :as config]
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.query-processor.middleware.cache-backend.interface :as i]
-            [metabase.query-processor.util :as qputil]
-            [metabase.util :as u]))
+            [metabase.query-processor.util :as qputil]))
 
 (def ^:dynamic ^Boolean *ignore-cached-results*
   "Should we force the query to run, ignoring cached results even if they're available?
diff --git a/src/metabase/query_processor/middleware/cache_backend/db.clj b/src/metabase/query_processor/middleware/cache_backend/db.clj
index 4964d8a88a83ad027b33f3781bbc0e965e568585..bb426ba5f3e1ab180b6b270c708853ff46cf476a 100644
--- a/src/metabase/query_processor/middleware/cache_backend/db.clj
+++ b/src/metabase/query_processor/middleware/cache_backend/db.clj
@@ -1,10 +1,12 @@
 (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]
+  (:require [metabase
+             [public-settings :as public-settings]
+             [util :as u]]
+            [metabase.models
+             [interface :as models]
+             [query-cache :refer [QueryCache]]]
             [metabase.query-processor.middleware.cache-backend.interface :as i]
-            [metabase.util :as u]))
+            [toucan.db :as db]))
 
 (defn- cached-results
   "Return cached results for QUERY-HASH if they exist and are newer than MAX-AGE-SECONDS."
diff --git a/src/metabase/query_processor/middleware/catch_exceptions.clj b/src/metabase/query_processor/middleware/catch_exceptions.clj
index 72c02cafa4254151313954b0a32de4eb5b2beaa8..b28c2409645bbf1861655cc5f1ec50d23038ff36 100644
--- a/src/metabase/query_processor/middleware/catch_exceptions.clj
+++ b/src/metabase/query_processor/middleware/catch_exceptions.clj
@@ -1,10 +1,10 @@
 (ns metabase.query-processor.middleware.catch-exceptions
   "Middleware for catching exceptions thrown by the query processor and returning them in a friendlier format."
-  (:require schema.utils
+  (:require [metabase.query-processor.middleware.expand-resolve :as expand-resolve]
             [metabase.query-processor.util :as qputil]
-            [metabase.query-processor.middleware.expand-resolve :as expand-resolve]
-            [metabase.util :as u])
-  (:import (schema.utils NamedError ValidationError)))
+            [metabase.util :as u]
+            schema.utils)
+  (:import [schema.utils NamedError ValidationError]))
 
 (defn- fail [query, ^Throwable e, & [additional-info]]
   (merge {:status         :failed
diff --git a/src/metabase/query_processor/middleware/dev.clj b/src/metabase/query_processor/middleware/dev.clj
index 3dc57b94ab0fbcae6e9b8702c8a13691b288e0f7..b0e00e9e66cddbae64c999527c30d4e4e72ffb92 100644
--- a/src/metabase/query_processor/middleware/dev.clj
+++ b/src/metabase/query_processor/middleware/dev.clj
@@ -1,9 +1,10 @@
 (ns metabase.query-processor.middleware.dev
   "Middleware that's only active in dev and test scenarios. These middleware functions do additional checks
    of query processor behavior that are undesirable in normal production use."
-  (:require [schema.core :as s]
-            (metabase [config :as config]
-                      [util :as u])))
+  (:require [metabase
+             [config :as config]
+             [util :as u]]
+            [schema.core :as s]))
 
 ;; The following are just assertions that check the behavior of the QP. It doesn't make sense to run them on prod because at best they
 ;; just waste CPU cycles and at worst cause a query to fail when it would otherwise succeed.
diff --git a/src/metabase/query_processor/middleware/expand_resolve.clj b/src/metabase/query_processor/middleware/expand_resolve.clj
index f5cdc5340f488324bfe00b89462667df630e2ea0..1fa626cbf7e50126336f8726eb095774b74d707a 100644
--- a/src/metabase/query_processor/middleware/expand_resolve.clj
+++ b/src/metabase/query_processor/middleware/expand_resolve.clj
@@ -1,11 +1,12 @@
 (ns metabase.query-processor.middleware.expand-resolve
   "Middleware for converting a MBQL query into an 'expanded' form that contains additional information needed by drivers for running queries,
    and resolving various referenced Fields and Tables."
-  (:require [toucan.db :as db]
-            [metabase.models.database :refer [Database]]
-            (metabase.query-processor [expand :as expand]
-                                      [resolve :as resolve]
-                                      [util :as qputil])))
+  (:require [metabase.models.database :refer [Database]]
+            [metabase.query-processor
+             [expand :as expand]
+             [resolve :as resolve]
+             [util :as qputil]]
+            [toucan.db :as db]))
 
 (def ^{:arglists '([query])} expand-and-resolve
   "Expand and resolve a QUERY.
diff --git a/src/metabase/query_processor/middleware/log.clj b/src/metabase/query_processor/middleware/log.clj
index fe8707f71354753bf012c140484e55f224425548..6ac5031e61c05de6f50fbaa8a26f7abf72efe870 100644
--- a/src/metabase/query_processor/middleware/log.clj
+++ b/src/metabase/query_processor/middleware/log.clj
@@ -1,11 +1,12 @@
 (ns metabase.query-processor.middleware.log
   "Middleware for logging a query before it is processed.
    (Various other middleware functions log the query as well in different stages.)"
-  (:require [clojure.walk :as walk]
-            [clojure.tools.logging :as log]
+  (:require [clojure.tools.logging :as log]
+            [clojure.walk :as walk]
             [medley.core :as m]
-            (metabase.query-processor [interface :as i]
-                                      [util :as qputil])
+            [metabase.query-processor
+             [interface :as i]
+             [util :as qputil]]
             [metabase.util :as u]))
 
 (defn- log-expanded-query* [query]
diff --git a/src/metabase/query_processor/middleware/mbql_to_native.clj b/src/metabase/query_processor/middleware/mbql_to_native.clj
index a3bb7f862335d27613ba80ee2ce3c3e3fd49b1c3..2179256597c55e5526d57a5f2674f7b9a85f5a49 100644
--- a/src/metabase/query_processor/middleware/mbql_to_native.clj
+++ b/src/metabase/query_processor/middleware/mbql_to_native.clj
@@ -2,10 +2,12 @@
   "Middleware responsible for converting MBQL queries to native queries (by calling the driver's QP methods)
    so the query can then be executed."
   (:require [clojure.tools.logging :as log]
-            [metabase.driver :as driver]
-            (metabase.query-processor [interface :as i]
-                                      [util :as qputil])
-            [metabase.util :as u]))
+            [metabase
+             [driver :as driver]
+             [util :as u]]
+            [metabase.query-processor
+             [interface :as i]
+             [util :as qputil]]))
 
 (defn- query->native-form
   "Return a `:native` query form for QUERY, converting it from MBQL if needed."
diff --git a/src/metabase/query_processor/parameters.clj b/src/metabase/query_processor/parameters.clj
index d3ac5416c555c4f1ef0f06ce0451bba6d7a177ce..ae3a3cbbdaf239d468c47037e356e7eaf6112ebe 100644
--- a/src/metabase/query_processor/parameters.clj
+++ b/src/metabase/query_processor/parameters.clj
@@ -1,14 +1,13 @@
 (ns metabase.query-processor.parameters
   "Code for handling parameter substitution in MBQL queries."
-  (:require [clojure.core.match :refer [match]]
+  (:require [clj-time
+             [core :as t]
+             [format :as tf]]
             [clojure.string :as s]
-            (clj-time [core :as t]
-                      [format :as tf])
             [medley.core :as m]
             [metabase.driver :as driver]
-            [metabase.query-processor.sql-parameters :as native-params]
-            [metabase.util :as u])
-  (:import (org.joda.time DateTimeConstants DateTime)))
+            [metabase.query-processor.sql-parameters :as native-params])
+  (:import [org.joda.time DateTime DateTimeConstants]))
 
 ;;; +-------------------------------------------------------------------------------------------------------+
 ;;; |                                    DATE RANGES & PERIODS                                              |
diff --git a/src/metabase/query_processor/permissions.clj b/src/metabase/query_processor/permissions.clj
index 8f7c8974ed140dba05d9cdaed53a006254d9bc99..5dd16462d78d2b1b618fb25cbe38cd50545cd68c 100644
--- a/src/metabase/query_processor/permissions.clj
+++ b/src/metabase/query_processor/permissions.clj
@@ -1,16 +1,12 @@
 (ns metabase.query-processor.permissions
   "Logic related to whether a given user has permissions to run/edit a given query."
-  ;; TODO - Some functions this namespace predates the newer implementations of Permissions checking on a model-by-model basis (and `*current-user-permissions-set*`)
-  ;;        They essentially do the same thing. This has better logging but the other has more flexibilitiy and is simpler to user.
-  ;;        At some point we should rework this namespace to use the newer approach.
   (:require [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
             [metabase.api.common :refer [*current-user-permissions-set*]]
-            [toucan.db :as db]
             [metabase.models.permissions :as perms]
             [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx]))
-
+            [metabase.util.honeysql-extensions :as hx]
+            [toucan.db :as db]))
 
 ;;; ------------------------------------------------------------ Helper Fns ------------------------------------------------------------
 
diff --git a/src/metabase/query_processor/resolve.clj b/src/metabase/query_processor/resolve.clj
index be5bd32c62c47fe2f372aa98bccb9851230dea42..1d1747d15df622cbecd0e6ae2281097218a87285 100644
--- a/src/metabase/query_processor/resolve.clj
+++ b/src/metabase/query_processor/resolve.clj
@@ -1,25 +1,20 @@
 (ns metabase.query-processor.resolve
   "Resolve references to `Fields`, `Tables`, and `Databases` in an expanded query dictionary."
   (:refer-clojure :exclude [resolve])
-  (:require (clojure [set :as set]
-                     [walk :as walk])
+  (:require [clojure
+             [set :as set]
+             [walk :as walk]]
             [medley.core :as m]
+            [metabase
+             [db :as mdb]
+             [util :as u]]
+            [metabase.models
+             [field :as field]
+             [table :refer [Table]]]
+            [metabase.query-processor.interface :as i]
             [schema.core :as s]
-            [toucan.db :as db]
-            [metabase.db :as mdb]
-            (metabase.models [field :as field]
-                             [table :refer [Table]])
-            [metabase.query-processor.interface :refer :all]
-            [metabase.util :as u])
-  (:import (metabase.query_processor.interface DateTimeField
-                                               DateTimeValue
-                                               ExpressionRef
-                                               Field
-                                               FieldPlaceholder
-                                               RelativeDatetime
-                                               RelativeDateTimeValue
-                                               Value
-                                               ValuePlaceholder)))
+            [toucan.db :as db])
+  (:import [metabase.query_processor.interface DateTimeField DateTimeValue ExpressionRef Field FieldPlaceholder RelativeDatetime RelativeDateTimeValue Value ValuePlaceholder]))
 
 ;; # ---------------------------------------------------------------------- UTIL FNS ------------------------------------------------------------
 
@@ -77,7 +72,7 @@
                       (assoc this :parent resolved)))
                   this)
     parent-id (assoc this :parent (or (field-id->field parent-id)
-                                      (map->FieldPlaceholder {:field-id parent-id})))
+                                      (i/map->FieldPlaceholder {:field-id parent-id})))
     :else     this))
 
 (defn- field-resolve-table [{:keys [table-id fk-field-id field-id], :as this} fk-id+table-id->table]
@@ -105,15 +100,15 @@
 
 (defn- field-ph-resolve-field [{:keys [field-id datetime-unit fk-field-id], :as this} field-id->field]
   (if-let [{:keys [base-type special-type], :as field} (some-> (field-id->field field-id)
-                                                               map->Field
+                                                               i/map->Field
                                                                (assoc :fk-field-id fk-field-id))]
     ;; try to resolve the Field with the ones available in field-id->field
     (let [datetime-field? (or (isa? base-type :type/DateTime)
                               (isa? special-type :type/DateTime))]
       (if-not datetime-field?
         field
-        (map->DateTimeField {:field field
-                             :unit  (or datetime-unit :day)}))) ; default to `:day` if a unit wasn't specified
+        (i/map->DateTimeField {:field field
+                               :unit  (or datetime-unit :day)}))) ; default to `:day` if a unit wasn't specified
     ;; If that fails just return ourselves as-is
     this))
 
@@ -133,21 +128,21 @@
 (extend-protocol IParseValueForField
   Field
   (parse-value [this value]
-    (s/validate Value (map->Value {:field this, :value value})))
+    (s/validate Value (i/map->Value {:field this, :value value})))
 
   ExpressionRef
   (parse-value [this value]
-    (s/validate Value (map->Value {:field this, :value value})))
+    (s/validate Value (i/map->Value {:field this, :value value})))
 
   DateTimeField
   (parse-value [this value]
     (cond
       (u/date-string? value)
-      (s/validate DateTimeValue (map->DateTimeValue {:field this, :value (u/->Timestamp value)}))
+      (s/validate DateTimeValue (i/map->DateTimeValue {:field this, :value (u/->Timestamp value)}))
 
       (instance? RelativeDatetime value)
       (do (s/validate RelativeDatetime value)
-          (s/validate RelativeDateTimeValue (map->RelativeDateTimeValue {:field this, :amount (:amount value), :unit (:unit value)})))
+          (s/validate RelativeDateTimeValue (i/map->RelativeDateTimeValue {:field this, :amount (:amount value), :unit (:unit value)})))
 
       (nil? value)
       nil
@@ -202,7 +197,7 @@
                                           :id [:in field-ids]))
                           (m/map-vals rename-mb-field-keys)
                           (m/map-vals #(assoc % :parent (when-let [parent-id (:parent-id %)]
-                                                          (map->FieldPlaceholder {:field-id parent-id})))))]
+                                                          (i/map->FieldPlaceholder {:field-id parent-id})))))]
           (->>
            ;; Now record the IDs of Tables these fields references in the :table-ids property of the expanded query dict.
            ;; Those will be used for Table resolution in the next step.
@@ -233,15 +228,15 @@
   [source-table-id fk-field-ids]
   (when (seq fk-field-ids)
     (vec (for [{:keys [source-field-name source-field-id target-field-id target-field-name target-table-id target-table-name target-table-schema]} (fk-field-ids->info source-table-id fk-field-ids)]
-           (map->JoinTable {:table-id     target-table-id
-                            :table-name   target-table-name
-                            :schema       target-table-schema
-                            :pk-field     (map->JoinTableField {:field-id   target-field-id
-                                                                :field-name target-field-name})
-                            :source-field (map->JoinTableField {:field-id   source-field-id
-                                                                :field-name source-field-name})
-                            ;; some DBs like Oracle limit the length of identifiers to 30 characters so only take the first 30 here
-                            :join-alias  (apply str (take 30 (str target-table-name "__via__" source-field-name)))})))))
+           (i/map->JoinTable {:table-id     target-table-id
+                              :table-name   target-table-name
+                              :schema       target-table-schema
+                              :pk-field     (i/map->JoinTableField {:field-id   target-field-id
+                                                                    :field-name target-field-name})
+                              :source-field (i/map->JoinTableField {:field-id   source-field-id
+                                                                    :field-name source-field-name})
+                              ;; some DBs like Oracle limit the length of identifiers to 30 characters so only take the first 30 here
+                              :join-alias  (apply str (take 30 (str target-table-name "__via__" source-field-name)))})))))
 
 (defn- resolve-tables
   "Resolve the `Tables` in an EXPANDED-QUERY-DICT."
diff --git a/src/metabase/query_processor/sql_parameters.clj b/src/metabase/query_processor/sql_parameters.clj
index a48cb7b71d810e10af732b03f382f4a5c1462b52..6c762c408335e564d9aeafa5c0f3c3336d4b9b56 100644
--- a/src/metabase/query_processor/sql_parameters.clj
+++ b/src/metabase/query_processor/sql_parameters.clj
@@ -3,18 +3,18 @@
    This is a new implementation, fondly referred to as 'SQL parameters 2.0', written for v0.23.0.
    The new implementation uses prepared statement args instead of substituting them directly into the query,
    and is much better-organized and better-documented."
-  (:require [clojure.tools.logging :as log]
-            [clojure.string :as str]
+  (:require [clojure.string :as str]
+            [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
-            [schema.core :as s]
-            [toucan.db :as db]
-            [metabase.models.field :refer [Field], :as field]
+            [metabase.models.field :as field :refer [Field]]
             [metabase.query-processor.expand :as ql]
             [metabase.util :as u]
-            [metabase.util.schema :as su])
-  (:import java.text.NumberFormat
-           clojure.lang.Keyword
+            [metabase.util.schema :as su]
+            [schema.core :as s]
+            [toucan.db :as db])
+  (:import clojure.lang.Keyword
            honeysql.types.SqlCall
+           java.text.NumberFormat
            metabase.models.field.FieldInstance))
 
 ;; The Basics:
diff --git a/src/metabase/query_processor/util.clj b/src/metabase/query_processor/util.clj
index dec320e4dc655fec44f52390c155f7996624686f..47f80946e2abff02fbc651b1287212716bfc57de 100644
--- a/src/metabase/query_processor/util.clj
+++ b/src/metabase/query_processor/util.clj
@@ -1,9 +1,9 @@
 (ns metabase.query-processor.util
   "Utility functions used by the global query processor and middleware functions."
-  (:require (buddy.core [codecs :as codecs]
-                        [hash :as hash])
-            [cheshire.core :as json]
-            [toucan.db :as db]))
+  (:require [buddy.core
+             [codecs :as codecs]
+             [hash :as hash]]
+            [cheshire.core :as json]))
 
 (defn mbql-query?
   "Is the given query an MBQL query?"
diff --git a/src/metabase/routes.clj b/src/metabase/routes.clj
index 0919e6983f58dc865392b38832dcde9d87b6cabd..44a7bcc36f95cc3ce67f0a946f7600335f925b1f 100644
--- a/src/metabase/routes.clj
+++ b/src/metabase/routes.clj
@@ -1,16 +1,18 @@
 (ns metabase.routes
-  (:require [clojure.java.io :as io]
+  (:require [cheshire.core :as json]
+            [clojure.java.io :as io]
             [clojure.string :as s]
-            [cheshire.core :as json]
-            (compojure [core :refer [context defroutes GET]]
-                       [route :as route])
-            [ring.util.response :as resp]
-            [stencil.core :as stencil]
+            [compojure
+             [core :refer [context defroutes GET]]
+             [route :as route]]
+            [metabase
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.api.routes :as api]
             [metabase.core.initialization-status :as init-status]
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u]
-            [metabase.util.embed :as embed]))
+            [metabase.util.embed :as embed]
+            [ring.util.response :as resp]
+            [stencil.core :as stencil]))
 
 (defn- base-href []
   (str (.getPath (clojure.java.io/as-url (public-settings/site-url))) "/"))
@@ -20,12 +22,12 @@
   ;; https://stackoverflow.com/questions/14780858/escape-in-script-tag-contents/23983448#23983448
   (s/replace text #"</script" "</scr\\\\ipt"))
 
-(defn- load-file [path]
+(defn- load-file-at-path [path]
   (slurp (or (io/resource path)
              (throw (Exception. (str "Cannot find '" path "'. Did you remember to build the Metabase frontend?"))))))
 
 (defn- load-template [path variables]
-  (stencil/render-string (load-file path) variables))
+  (stencil/render-string (load-file-at-path path) variables))
 
 (defn- entrypoint [entry embeddable? {:keys [uri]}]
   (-> (if (init-status/complete?)
@@ -34,7 +36,7 @@
                         :uri            (escape-script (json/generate-string uri))
                         :base_href      (escape-script (json/generate-string (base-href)))
                         :embed_code     (when embeddable? (embed/head uri))})
-        (load-file "frontend_client/init.html"))
+        (load-file-at-path "frontend_client/init.html"))
       resp/response
       (resp/content-type "text/html; charset=utf-8")))
 
diff --git a/src/metabase/sample_data.clj b/src/metabase/sample_data.clj
index c7c0c4a44ad271ba279b9560a91cc17f35c8274e..4d839df062376e4c7e025cc1c46e613cb47cfaa4 100644
--- a/src/metabase/sample_data.clj
+++ b/src/metabase/sample_data.clj
@@ -2,10 +2,11 @@
   (:require [clojure.java.io :as io]
             [clojure.string :as s]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
+            [metabase
+             [sync-database :as sync-database]
+             [util :as u]]
             [metabase.models.database :refer [Database]]
-            [metabase.sync-database :as sync-database]
-            [metabase.util :as u]))
+            [toucan.db :as db]))
 
 (def ^:private ^:const ^String sample-dataset-name     "Sample Dataset")
 (def ^:private ^:const ^String sample-dataset-filename "sample-dataset.db.mv.db")
diff --git a/src/metabase/sync_database.clj b/src/metabase/sync_database.clj
index e0bb506e78b35f87ad254d086f28fc4a32077cde..1e85c474c2cbe11d7027765964016618776a3216 100644
--- a/src/metabase/sync_database.clj
+++ b/src/metabase/sync_database.clj
@@ -1,18 +1,20 @@
 (ns metabase.sync-database
   "The logic for doing DB and Table syncing itself."
   (:require [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            [metabase.events :as events]
-            [metabase.models.raw-table :as raw-table]
-            [metabase.models.table :as table]
+            [metabase
+             [driver :as driver]
+             [events :as events]
+             [util :as u]]
+            [metabase.models
+             [raw-table :as raw-table]
+             [table :as table]]
             [metabase.query-processor.interface :as i]
-            (metabase.sync-database [analyze :as analyze]
-                                    [introspect :as introspect]
-                                    [sync :as sync]
-                                    [sync-dynamic :as sync-dynamic])
-            [metabase.util :as u]))
-
+            [metabase.sync-database
+             [analyze :as analyze]
+             [introspect :as introspect]
+             [sync :as sync]
+             [sync-dynamic :as sync-dynamic]]
+            [toucan.db :as db]))
 
 (declare sync-database-with-tracking!
          sync-table-with-tracking!)
diff --git a/src/metabase/sync_database/analyze.clj b/src/metabase/sync_database/analyze.clj
index bad37a0c7a53b7e267668e923e7fc08daa921f1b..7e6fe48da84b832525da4c51a63b6b9168879e34 100644
--- a/src/metabase/sync_database/analyze.clj
+++ b/src/metabase/sync_database/analyze.clj
@@ -1,18 +1,20 @@
 (ns metabase.sync-database.analyze
   "Functions which handle the in-depth data shape analysis portion of the sync process."
-  (:require [clojure.math.numeric-tower :as math]
+  (:require [cheshire.core :as json]
+            [clojure.math.numeric-tower :as math]
             [clojure.string :as s]
             [clojure.tools.logging :as log]
-            [cheshire.core :as json]
-            [schema.core :as schema]
-            [toucan.db :as db]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
             [metabase.db.metadata-queries :as queries]
-            [metabase.driver :as driver]
-            (metabase.models [field :as field]
-                             [field-values :as field-values]
-                             [table :as table])
+            [metabase.models
+             [field :as field]
+             [field-values :as field-values]
+             [table :as table]]
             [metabase.sync-database.interface :as i]
-            [metabase.util :as u]))
+            [schema.core :as schema]
+            [toucan.db :as db]))
 
 (def ^:private ^:const ^Float percent-valid-url-threshold
   "Fields that have at least this percent of values that are valid URLs should be given a special type of `:type/URL`."
diff --git a/src/metabase/sync_database/interface.clj b/src/metabase/sync_database/interface.clj
index cdccf509ff960dcf440d0f32c6e75f64fef998bc..bc931754bb0409478d29d9214457411d90b20f71 100644
--- a/src/metabase/sync_database/interface.clj
+++ b/src/metabase/sync_database/interface.clj
@@ -1,9 +1,7 @@
 (ns metabase.sync-database.interface
   "Schemas describing the output expected from different DB sync functions."
-  (:require [schema.core :as s]
-            [metabase.models.field :as field]
-            [metabase.util.schema :as su]))
-
+  (:require [metabase.util.schema :as su]
+            [schema.core :as s]))
 
 (def AnalyzeTable
   "Schema for the expected output of `analyze-table`."
diff --git a/src/metabase/sync_database/introspect.clj b/src/metabase/sync_database/introspect.clj
index d900697003cbf1e535af31f3ff02aca41210b0d0..aaa0d2800a36b5a474cf40879781cc14d4b7e5c8 100644
--- a/src/metabase/sync_database/introspect.clj
+++ b/src/metabase/sync_database/introspect.clj
@@ -2,13 +2,15 @@
   "Functions which handle the raw sync process."
   (:require [clojure.set :as set]
             [clojure.tools.logging :as log]
-            [schema.core :as schema]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            (metabase.models [raw-column :refer [RawColumn]]
-                             [raw-table :refer [RawTable]])
+            [metabase
+             [driver :as driver]
+             [util :as u]]
+            [metabase.models
+             [raw-column :refer [RawColumn]]
+             [raw-table :refer [RawTable]]]
             [metabase.sync-database.interface :as i]
-            [metabase.util :as u]))
+            [schema.core :as schema]
+            [toucan.db :as db]))
 
 (defn- named-table
   ([table]
diff --git a/src/metabase/sync_database/sync.clj b/src/metabase/sync_database/sync.clj
index c0426f79b45a79acccbb2710d2ffe785b1912ad5..a97187a098a0321bfc1472f6e739556c5fa7b50f 100644
--- a/src/metabase/sync_database/sync.clj
+++ b/src/metabase/sync_database/sync.clj
@@ -1,18 +1,20 @@
 (ns metabase.sync-database.sync
-  (:require (clojure [set :as set]
-                     [string :as s])
+  (:require [clojure
+             [set :as set]
+             [string :as s]]
             [clojure.tools.logging :as log]
-            [toucan.db :as db]
-            [metabase.db :as mdb]
-            [metabase.driver :as driver]
-            (metabase.models [field :refer [Field], :as field]
-                             [raw-column :refer [RawColumn]]
-                             [raw-table :refer [RawTable], :as raw-table]
-                             [table :refer [Table], :as table])
-            [metabase.util :as u])
+            [metabase
+             [db :as mdb]
+             [driver :as driver]
+             [util :as u]]
+            [metabase.models
+             [field :as field :refer [Field]]
+             [raw-column :refer [RawColumn]]
+             [raw-table :as raw-table :refer [RawTable]]
+             [table :as table :refer [Table]]]
+            [toucan.db :as db])
   (:import metabase.models.raw_table.RawTableInstance))
 
-
 ;;; ------------------------------------------------------------ FKs ------------------------------------------------------------
 
 (defn- save-fks!
diff --git a/src/metabase/sync_database/sync_dynamic.clj b/src/metabase/sync_database/sync_dynamic.clj
index 9d719d57245aa7f62d33458652c2afdec3f77383..1d3d7c7a1dd57d41641e78c1b9bcd47b1d2e1ef6 100644
--- a/src/metabase/sync_database/sync_dynamic.clj
+++ b/src/metabase/sync_database/sync_dynamic.clj
@@ -1,18 +1,19 @@
 (ns metabase.sync-database.sync-dynamic
   "Functions for syncing drivers with `:dynamic-schema` which have no fixed definition of their data."
   (:require [clojure.set :as set]
-            [clojure.string :as s]
             [clojure.tools.logging :as log]
+            [metabase
+             [driver :as driver]
+             [util :as u]]
+            [metabase.models
+             [field :as field :refer [Field]]
+             [raw-table :as raw-table :refer [RawTable]]
+             [table :as table :refer [Table]]]
+            [metabase.sync-database
+             [interface :as i]
+             [sync :as sync]]
             [schema.core :as schema]
-            [toucan.db :as db]
-            [metabase.driver :as driver]
-            (metabase.models [field :refer [Field], :as field]
-                             [raw-table :refer [RawTable], :as raw-table]
-                             [table :refer [Table], :as table])
-            (metabase.sync-database [interface :as i]
-                                    [sync :as sync])
-            [metabase.util :as u]))
-
+            [toucan.db :as db]))
 
 (defn- save-nested-fields!
   "Save any nested `Fields` for a given parent `Field`.
diff --git a/src/metabase/task/follow_up_emails.clj b/src/metabase/task/follow_up_emails.clj
index 6270c30f4001c83b7cd73f215b986fa97d588c4d..5f5add22bef8db9d4bfb3e203a8a53c1a3e19ac3 100644
--- a/src/metabase/task/follow_up_emails.clj
+++ b/src/metabase/task/follow_up_emails.clj
@@ -1,21 +1,24 @@
 (ns metabase.task.follow-up-emails
   "Tasks which follow up with Metabase users."
-  (:require [clojure.tools.logging :as log]
-            (clj-time [coerce :as c]
-                      [core :as t])
-            [clojurewerkz.quartzite.jobs :as jobs]
+  (:require [clj-time
+             [coerce :as c]
+             [core :as t]]
+            [clojure.tools.logging :as log]
+            [clojurewerkz.quartzite
+             [jobs :as jobs]
+             [triggers :as triggers]]
             [clojurewerkz.quartzite.schedule.cron :as cron]
-            [clojurewerkz.quartzite.triggers :as triggers]
-            [toucan.db :as db]
-            [metabase.email :as email]
+            [metabase
+             [email :as email]
+             [public-settings :as public-settings]
+             [task :as task]]
             [metabase.email.messages :as messages]
-            (metabase.models [activity :refer [Activity]]
-                             [setting :as setting]
-                             [user :as user, :refer [User]]
-                             [view-log :refer [ViewLog]])
-            [metabase.public-settings :as public-settings]
-            [metabase.task :as task]))
-
+            [metabase.models
+             [activity :refer [Activity]]
+             [setting :as setting]
+             [user :as user :refer [User]]
+             [view-log :refer [ViewLog]]]
+            [toucan.db :as db]))
 
 (declare send-follow-up-email! send-abandonment-email!)
 
diff --git a/src/metabase/task/send_anonymous_stats.clj b/src/metabase/task/send_anonymous_stats.clj
index c6b2143c8860fcaa278f7bbf7694c9bae3a3944d..bba54f80287ad535724af5d030984fdd68a93b2f 100644
--- a/src/metabase/task/send_anonymous_stats.clj
+++ b/src/metabase/task/send_anonymous_stats.clj
@@ -1,12 +1,13 @@
 (ns metabase.task.send-anonymous-stats
   "Contains a Metabase task which periodically sends anonymous usage information to the Metabase team."
   (:require [clojure.tools.logging :as log]
-            [clojurewerkz.quartzite.jobs :as jobs]
+            [clojurewerkz.quartzite
+             [jobs :as jobs]
+             [triggers :as triggers]]
             [clojurewerkz.quartzite.schedule.cron :as cron]
-            [clojurewerkz.quartzite.triggers :as triggers]
-            (metabase [config :as config]
-                      [public-settings :as public-settings]
-                      [task :as task])
+            [metabase
+             [public-settings :as public-settings]
+             [task :as task]]
             [metabase.util.stats :as stats]))
 
 (def ^:private ^:const job-key     "metabase.task.anonymous-stats.job")
diff --git a/src/metabase/task/send_pulses.clj b/src/metabase/task/send_pulses.clj
index 757f22446e753dd3b547d4c7c2cfcd6444a5e87a..266b1d2b2fe2b4bda6d4a4ba2a0824847e3a5573 100644
--- a/src/metabase/task/send_pulses.clj
+++ b/src/metabase/task/send_pulses.clj
@@ -1,17 +1,20 @@
 (ns metabase.task.send-pulses
   "Tasks related to running `Pulses`."
-  (:require [clojure.tools.logging :as log]
-            [clojurewerkz.quartzite.jobs :as jobs]
+  (:require [clj-time
+             [core :as time]
+             [predicates :as timepr]]
+            [clojure.tools.logging :as log]
+            [clojurewerkz.quartzite
+             [jobs :as jobs]
+             [triggers :as triggers]]
             [clojurewerkz.quartzite.schedule.cron :as cron]
-            [clojurewerkz.quartzite.triggers :as triggers]
-            (clj-time [core :as time]
-                      [predicates :as timepr])
-            (metabase.models [pulse :refer [Pulse], :as pulse]
-                             [pulse-channel :as pulse-channel]
-                             [setting :as setting])
-            [metabase.pulse :as p]
-            [metabase.task :as task]))
-
+            [metabase
+             [pulse :as p]
+             [task :as task]]
+            [metabase.models
+             [pulse :as pulse]
+             [pulse-channel :as pulse-channel]
+             [setting :as setting]]))
 
 (declare send-pulses!)
 
diff --git a/src/metabase/task/sync_databases.clj b/src/metabase/task/sync_databases.clj
index 371be7ace60379402d72912b43b9f16e14d81169..9fd4816d2b518bfc1ab77ca234d036d33f24145e 100644
--- a/src/metabase/task/sync_databases.clj
+++ b/src/metabase/task/sync_databases.clj
@@ -1,14 +1,16 @@
 (ns metabase.task.sync-databases
   (:require [clj-time.core :as t]
             [clojure.tools.logging :as log]
-            [clojurewerkz.quartzite.jobs :as jobs]
+            [clojurewerkz.quartzite
+             [jobs :as jobs]
+             [triggers :as triggers]]
             [clojurewerkz.quartzite.schedule.cron :as cron]
-            [clojurewerkz.quartzite.triggers :as triggers]
-            [toucan.db :as db]
-            [metabase.task :as task]
-            [metabase.driver :as driver]
+            [metabase
+             [driver :as driver]
+             [sync-database :as sync-database]
+             [task :as task]]
             [metabase.models.database :refer [Database]]
-            [metabase.sync-database :as sync-database]))
+            [toucan.db :as db]))
 
 (def ^:private ^:const sync-databases-job-key     "metabase.task.sync-databases.job")
 (def ^:private ^:const sync-databases-trigger-key "metabase.task.sync-databases.trigger")
diff --git a/src/metabase/task/upgrade_checks.clj b/src/metabase/task/upgrade_checks.clj
index 21a922c736d5f2bf262ea3a2d2df50d533350a6b..47b8f7e54a5c721986193be8470ed1a271e3754e 100644
--- a/src/metabase/task/upgrade_checks.clj
+++ b/src/metabase/task/upgrade_checks.clj
@@ -1,14 +1,16 @@
 (ns metabase.task.upgrade-checks
   "Contains a Metabase task which periodically checks for the availability of new Metabase versions."
-  (:require [clojure.tools.logging :as log]
-            [cheshire.core :as json]
+  (:require [cheshire.core :as json]
             [clj-http.client :as http]
-            [clojurewerkz.quartzite.jobs :as jobs]
+            [clojure.tools.logging :as log]
+            [clojurewerkz.quartzite
+             [jobs :as jobs]
+             [triggers :as triggers]]
             [clojurewerkz.quartzite.schedule.cron :as cron]
-            [clojurewerkz.quartzite.triggers :as triggers]
-            (metabase [config :as config]
-                      [public-settings :as public-settings]
-                      [task :as task])))
+            [metabase
+             [config :as config]
+             [public-settings :as public-settings]
+             [task :as task]]))
 
 (def ^:private ^:const job-key     "metabase.task.upgrade-checks.job")
 (def ^:private ^:const trigger-key "metabase.task.upgrade-checks.trigger")
diff --git a/src/metabase/util.clj b/src/metabase/util.clj
index d73dc84cf240d6b47312984f5ef8b545f9d7baaf..9b79d0c0302c810a830fd28021be92a1e1b541ba 100644
--- a/src/metabase/util.clj
+++ b/src/metabase/util.clj
@@ -1,27 +1,27 @@
 (ns metabase.util
   "Common utility functions useful throughout the codebase."
-  (:require [clojure.data :as data]
-            (clojure.java [classpath :as classpath]
-                          [jdbc :as jdbc])
+  (:require [clj-time
+             [coerce :as coerce]
+             [core :as t]
+             [format :as time]]
+            [clojure
+             [data :as data]
+             [pprint :refer [pprint]]
+             [string :as s]]
+            [clojure.java
+             [classpath :as classpath]
+             [jdbc :as jdbc]]
             [clojure.math.numeric-tower :as math]
-            (clojure [pprint :refer [pprint]]
-                     [string :as s])
             [clojure.tools.logging :as log]
             [clojure.tools.namespace.find :as ns-find]
-            (clj-time [core :as t]
-                      [coerce :as coerce]
-                      [format :as time])
-            colorize.core
-            [ring.util.codec :as codec]
+            colorize.core ; this needs to be loaded for `format-color`
             [metabase.config :as config]
-            metabase.logger)             ; make sure this is loaded since we use clojure.tools.logging here
+            [ring.util.codec :as codec])
   (:import clojure.lang.Keyword
-           (java.net Socket
-                     InetSocketAddress
-                     InetAddress)
-           (java.sql SQLException Timestamp)
-           (java.text Normalizer Normalizer$Form)
-           (java.util Calendar Date TimeZone)
+           [java.net InetAddress InetSocketAddress Socket]
+           [java.sql SQLException Timestamp]
+           [java.text Normalizer Normalizer$Form]
+           [java.util Calendar Date TimeZone]
            javax.xml.bind.DatatypeConverter
            org.joda.time.DateTime
            org.joda.time.format.DateTimeFormatter))
@@ -831,3 +831,28 @@
       (if-let [new-index (s/index-of s substr index)]
         (recur (inc new-index) (inc cnt))
         cnt))))
+
+(defn select-non-nil-keys
+  "Like `select-keys`, but returns a map only containing keys in KS that are present *and non-nil* in M.
+
+     (select-non-nil-keys {:a 100, :b nil} #{:a :b :c})
+     ;; -> {:a 100}"
+  [m ks]
+  (into {} (for [k     ks
+                 :when (not (nil? (get m k)))]
+             {k (get m k)})))
+
+(defn select-keys-when
+  "Returns a map that only contains keys that are either `:present` or `:non-nil`.
+   Combines behavior of `select-keys` and `select-non-nil-keys`.
+   This is useful for API endpoints that update a model, which often have complex rules about what gets updated
+   (some keys are updated if `nil`, others only if non-nil).
+
+     (select-keys-when {:a 100, :b nil, :d 200, :e nil}
+       :present #{:a :b :c}
+       :non-nil #{:d :e :f})
+     ;; -> {:a 100, :b nil, :d 200}"
+  {:style/indent 1}
+  [m & {:keys [present non-nil]}]
+  (merge (select-keys m present)
+         (select-non-nil-keys m non-nil)))
diff --git a/src/metabase/util/embed.clj b/src/metabase/util/embed.clj
index 41f829cff4732276e26aff14e96b1011f0e5f2f8..e471d19c87809da9607613f13d0bec862f9782ef 100644
--- a/src/metabase/util/embed.clj
+++ b/src/metabase/util/embed.clj
@@ -1,14 +1,15 @@
 (ns metabase.util.embed
   "Utility functions for public links and embedding."
-  (:require [clojure.string :as str]
-            [buddy.core.codecs :as codecs]
-            (buddy.sign [jwt :as jwt]
-                        [util :as buddy-util])
+  (:require [buddy.core.codecs :as codecs]
+            [buddy.sign
+             [jwt :as jwt]
+             [util :as buddy-util]]
             [cheshire.core :as json]
-            [ring.util.codec :as codec]
+            [clojure.string :as str]
             [hiccup.core :refer [html]]
             [metabase.models.setting :as setting]
-            [metabase.public-settings :as public-settings]))
+            [metabase.public-settings :as public-settings]
+            [ring.util.codec :as codec]))
 
 ;;; ------------------------------------------------------------ PUBLIC LINKS UTIL FNS ------------------------------------------------------------
 
diff --git a/src/metabase/util/encryption.clj b/src/metabase/util/encryption.clj
index 8817bbb63eea67e7e6eba7f27b9453c9a509ce0d..4b2ae156a42d60e2775540716f70bfe67de67c69 100644
--- a/src/metabase/util/encryption.clj
+++ b/src/metabase/util/encryption.clj
@@ -1,13 +1,14 @@
 (ns metabase.util.encryption
   "Utility functions for encrypting and decrypting strings using AES256 CBC + HMAC SHA512 and the `MB_ENCRYPTION_SECRET_KEY` env var."
-  (:require (buddy.core [codecs :as codecs]
-                        [crypto :as crypto]
-                        [kdf :as kdf]
-                        [nonce :as nonce])
+  (:require [buddy.core
+             [codecs :as codecs]
+             [crypto :as crypto]
+             [kdf :as kdf]
+             [nonce :as nonce]]
             [clojure.tools.logging :as log]
             [environ.core :as env]
-            [ring.util.codec :as codec]
-            [metabase.util :as u]))
+            [metabase.util :as u]
+            [ring.util.codec :as codec]))
 
 (defn secret-key->hash
   "Generate a 64-byte byte array hash of SECRET-KEY using 100,000 iterations of PBKDF2+SHA512."
diff --git a/src/metabase/util/schema.clj b/src/metabase/util/schema.clj
index bae523e572d24f35e0fb4efe102116c1d695b0a5..0edf6185e910fe1ca4f069ff8f3fb2f922d13eef 100644
--- a/src/metabase/util/schema.clj
+++ b/src/metabase/util/schema.clj
@@ -1,11 +1,11 @@
 (ns metabase.util.schema
   "Various schemas that are useful throughout the app."
-  (:require [clojure.string :as str]
-            [cheshire.core :as json]
-            [schema.core :as s]
-            metabase.types
+  (:require [cheshire.core :as json]
+            [clojure.string :as str]
+            [medley.core :as m]
             [metabase.util :as u]
-            [metabase.util.password :as password]))
+            [metabase.util.password :as password]
+            [schema.core :as s]))
 
 (defn with-api-error-message
   "Return SCHEMA with an additional API-ERROR-MESSAGE that will be used to explain the error if a parameter fails validation."
@@ -41,6 +41,12 @@
       (when (instance? schema.core.EnumSchema schema)
         (format "value must be one of: %s." (str/join ", " (for [v (sort (:vs schema))]
                                                              (str "`" v "`")))))
+      ;; For cond-pre schemas we'll generate something like
+      ;; value must satisfy one of the following requirements: 1) value must be a boolean. 2) value must be a valid boolean string ('true' or 'false').
+      (when (instance? schema.core.CondPre schema)
+        (str "value must satisfy one of the following requirements: "
+             (str/join " " (for [[i child-schema] (m/indexed (:schemas schema))]
+                             (format "%d) %s" (inc i) (api-error-message child-schema))))))
       ;; do the same for sequences of a schema
       (when (vector? schema)
         (str "value must be an array." (when (= (count schema) 1)
@@ -113,7 +119,7 @@
   "Schema for a string that is a valid representation of a boolean (either `true` or `false`).
    Something that adheres to this schema is guaranteed to to work with `Boolean/parseBoolean`."
   (with-api-error-message (s/constrained s/Str boolean-string?)
-    "value must be a valid boolean (true or false)."))
+    "value must be a valid boolean string ('true' or 'false')."))
 
 (def JSONString
   "Schema for a string that is valid serialized JSON."
diff --git a/src/metabase/util/stats.clj b/src/metabase/util/stats.clj
index 953b56b0d71e33876085c631a00ee527ef5cb7ed..5d747e6788bb509dbdf24f837853f86a53189535 100644
--- a/src/metabase/util/stats.clj
+++ b/src/metabase/util/stats.clj
@@ -1,34 +1,36 @@
 (ns metabase.util.stats
   "Functions which summarize the usage of an instance"
-  (:require [clojure.tools.logging :as log]
-            [clj-http.client :as client]
+  (:require [clj-http.client :as client]
+            [clojure.tools.logging :as log]
             [medley.core :as m]
-            [toucan.db :as db]
+            [metabase
+             [config :as config]
+             [driver :as driver]
+             [email :as email]
+             [public-settings :as public-settings]
+             [util :as u]]
             [metabase.api.session :as session-api]
-            [metabase.config :as config]
-            [metabase.driver :as driver]
-            [metabase.email :as email]
             [metabase.integrations.slack :as slack]
-            (metabase.models [card :refer [Card]]
-                             [card-label :refer [CardLabel]]
-                             [collection :refer [Collection]]
-                             [dashboard :refer [Dashboard]]
-                             [dashboard-card :refer [DashboardCard]]
-                             [database :refer [Database]]
-                             [field :refer [Field]]
-                             [humanization :as humanization]
-                             [label :refer [Label]]
-                             [metric :refer [Metric]]
-                             [permissions-group :refer [PermissionsGroup]]
-                             [pulse :refer [Pulse]]
-                             [pulse-card :refer [PulseCard]]
-                             [pulse-channel :refer [PulseChannel]]
-                             [query-execution :refer [QueryExecution]]
-                             [segment :refer [Segment]]
-                             [table :refer [Table]]
-                             [user :refer [User]])
-            [metabase.public-settings :as public-settings]
-            [metabase.util :as u])
+            [metabase.models
+             [card :refer [Card]]
+             [card-label :refer [CardLabel]]
+             [collection :refer [Collection]]
+             [dashboard :refer [Dashboard]]
+             [dashboard-card :refer [DashboardCard]]
+             [database :refer [Database]]
+             [field :refer [Field]]
+             [humanization :as humanization]
+             [label :refer [Label]]
+             [metric :refer [Metric]]
+             [permissions-group :refer [PermissionsGroup]]
+             [pulse :refer [Pulse]]
+             [pulse-card :refer [PulseCard]]
+             [pulse-channel :refer [PulseChannel]]
+             [query-execution :refer [QueryExecution]]
+             [segment :refer [Segment]]
+             [table :refer [Table]]
+             [user :refer [User]]]
+            [toucan.db :as db])
   (:import java.util.Date))
 
 (defn- merge-count-maps
diff --git a/src/metabase/util/urls.clj b/src/metabase/util/urls.clj
index 3c27b5ee2033afecfc12bad23d45101565f0e1e0..76f9d6c428d6212197ff6ff94df38f90c728f2cc 100644
--- a/src/metabase/util/urls.clj
+++ b/src/metabase/util/urls.clj
@@ -5,8 +5,7 @@
 
    Functions for generating URLs not related to Metabase *objects* generally do not belong here, unless they are used in many places in the
    codebase; one-off URL-generation functions should go in the same namespaces or modules where they are used."
-  (:require [clojure.string :as s]
-            [metabase.public-settings :as public-settings]))
+  (:require [metabase.public-settings :as public-settings]))
 
 (defn pulse-url
   "Return an appropriate URL for a `Pulse` with ID.
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index a42630ea44f06bb291c5756a1f8802529f291577..a60512e787d8f6453ea796d7761810b47c0123d6 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -649,7 +649,7 @@
 
 ;; Test that we *cannot* share a Card if the Card has been archived
 (expect
-  "Not found."
+  "The object has been archived."
   (tu/with-temporary-setting-values [enable-public-sharing true]
     (tt/with-temp Card [card {:archived true}]
       ((user->client :crowberto) :post 404 (format "card/%d/public_link" (u/get-id card))))))
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 3e773b5614ed76b0d33f769f6ca3019278825c6e..74b15e60bf611174e203e7917f24a9ee2aee88cc 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -87,7 +87,8 @@
                                                     :parameters "abc"}))
 
 (def ^:private ^:const dashboard-defaults
-  {:caveats                 nil
+  {:archived                false
+   :caveats                 nil
    :created_at              true ; assuming you call dashboard-response on the results
    :description             nil
    :embedding_params        nil
@@ -95,6 +96,7 @@
    :made_public_by_id       nil
    :parameters              []
    :points_of_interest      nil
+   :position                nil
    :public_uuid             nil
    :show_in_getting_started false
    :updated_at              true})
diff --git a/test/metabase/api/embed_test.clj b/test/metabase/api/embed_test.clj
index 8abbdd460aa1476e453280354467226ba85d93ef..6c94ef15616e66e8bf7ccae8e07aee3d348d3cd7 100644
--- a/test/metabase/api/embed_test.clj
+++ b/test/metabase/api/embed_test.clj
@@ -74,7 +74,8 @@
    :display                "table"
    :visualization_settings {}
    :dataset_query          {:type "query"}
-   :parameters             ()})
+   :parameters             ()
+   :param_values           nil})
 
 (def successful-dashboard-info
   {:description nil, :parameters (), :ordered_cards (), :param_values nil})
diff --git a/test/metabase/api/metric_test.clj b/test/metabase/api/metric_test.clj
index 925031a4d499c1e05ae67debece2cf3a656adf08..3cb39bbee62e3fe5b7d6af4dc5d7d7e62544d5f6 100644
--- a/test/metabase/api/metric_test.clj
+++ b/test/metabase/api/metric_test.clj
@@ -15,6 +15,17 @@
 
 ;; ## Helper Fns
 
+(def ^:private ^:const metric-defaults
+  {:description             nil
+   :show_in_getting_started false
+   :caveats                 nil
+   :points_of_interest      nil
+   :how_is_this_calculated  nil
+   :created_at              true
+   :updated_at              true
+   :is_active               true
+   :definition              {}})
+
 (defn- user-details [user]
   (tu/match-$ user
     {:id           $
@@ -72,19 +83,13 @@
                                                  :definition "foobar"}))
 
 (expect
-  {:name                    "A Metric"
-   :description             "I did it!"
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :how_is_this_calculated  nil
-   :creator_id              (user->id :crowberto)
-   :creator                 (user-details (fetch-user :crowberto))
-   :created_at              true
-   :updated_at              true
-   :is_active               true
-   :definition              {:database 21
-                             :query    {:filter ["abc"]}}}
+  (merge metric-defaults
+         {:name        "A Metric"
+          :description "I did it!"
+          :creator_id  (user->id :crowberto)
+          :creator     (user-details (fetch-user :crowberto))
+          :definition  {:database 21
+                        :query    {:filter ["abc"]}}})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{:keys [id]} {:db_id database-id}]]
     (metric-response ((user->client :crowberto) :post 200 "metric" {:name                    "A Metric"
@@ -107,39 +112,37 @@
                                               :revision_message "something different"}))
 
 ;; test validations
-(expect {:errors {:name "value must be a non-blank string."}}
+(expect
+  {:errors {:name "value must be a non-blank string."}}
   ((user->client :crowberto) :put 400 "metric/1" {}))
 
-(expect {:errors {:revision_message "value must be a non-blank string."}}
+(expect
+  {:errors {:revision_message "value must be a non-blank string."}}
   ((user->client :crowberto) :put 400 "metric/1" {:name "abc"}))
 
-(expect {:errors {:revision_message "value must be a non-blank string."}}
+(expect
+  {:errors {:revision_message "value must be a non-blank string."}}
   ((user->client :crowberto) :put 400 "metric/1" {:name             "abc"
-                                                   :revision_message ""}))
+                                                  :revision_message ""}))
 
-(expect {:errors {:definition "value must be a map."}}
+(expect
+  {:errors {:definition "value must be a map."}}
   ((user->client :crowberto) :put 400 "metric/1" {:name             "abc"
-                                                   :revision_message "123"}))
+                                                  :revision_message "123"}))
 
-(expect {:errors {:definition "value must be a map."}}
+(expect
+  {:errors {:definition "value must be a map."}}
   ((user->client :crowberto) :put 400 "metric/1" {:name             "abc"
-                                                   :revision_message "123"
-                                                   :definition       "foobar"}))
+                                                  :revision_message "123"
+                                                  :definition       "foobar"}))
 
 (expect
-  {:name                    "Costa Rica"
-   :description             nil
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :how_is_this_calculated  nil
-   :creator_id              (user->id :rasta)
-   :creator                 (user-details (fetch-user :rasta))
-   :created_at              true
-   :updated_at              true
-   :is_active               true
-   :definition              {:database 2
-                             :query    {:filter ["not" "the toucans you're looking for"]}}}
+  (merge metric-defaults
+         {:name       "Costa Rica"
+          :creator_id (user->id :rasta)
+          :creator    (user-details (fetch-user :rasta))
+          :definition {:database 2
+                       :query    {:filter ["not" "the toucans you're looking for"]}}})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id :id} {:db_id database-id}]
                   Metric   [{:keys [id]} {:table_id table-id}]]
@@ -172,18 +175,12 @@
 
 (expect
   [{:success true}
-   {:name                    "Toucans in the rainforest"
-    :description             "Lookin' for a blueberry"
-    :show_in_getting_started false
-    :caveats                 nil
-    :points_of_interest      nil
-    :how_is_this_calculated  nil
-    :creator_id              (user->id :rasta)
-    :creator                 (user-details (fetch-user :rasta))
-    :created_at              true
-    :updated_at              true
-    :is_active               false
-    :definition              {}}]
+   (merge metric-defaults
+          {:name        "Toucans in the rainforest"
+           :description "Lookin' for a blueberry"
+           :creator_id  (user->id :rasta)
+           :creator     (user-details (fetch-user :rasta))
+           :is_active   false})]
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id :id} {:db_id database-id}]
                   Metric   [{:keys [id]}   {:table_id table-id}]]
@@ -199,18 +196,11 @@
 
 
 (expect
-  {:name                    "Toucans in the rainforest"
-   :description             "Lookin' for a blueberry"
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :how_is_this_calculated  nil
-   :creator_id              (user->id :crowberto)
-   :creator                 (user-details (fetch-user :crowberto))
-   :created_at              true
-   :updated_at              true
-   :is_active               true
-   :definition              {}}
+  (merge metric-defaults
+         {:name        "Toucans in the rainforest"
+          :description "Lookin' for a blueberry"
+          :creator_id  (user->id :crowberto)
+          :creator     (user-details (fetch-user :crowberto))})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id :id} {:db_id database-id}]
                   Metric   [{:keys [id]}   {:creator_id  (user->id :crowberto)
diff --git a/test/metabase/api/public_test.clj b/test/metabase/api/public_test.clj
index d7e49595a801bb4c81ce8535086b323deca98fd7..1d4b70c5827165bb831fa52e4bb68beb36442243 100644
--- a/test/metabase/api/public_test.clj
+++ b/test/metabase/api/public_test.clj
@@ -83,11 +83,30 @@
 
 ;; Check that we can fetch a PublicCard
 (expect
-  #{:dataset_query :description :display :id :name :visualization_settings}
+  #{:dataset_query :description :display :id :name :visualization_settings :param_values}
   (tu/with-temporary-setting-values [enable-public-sharing true]
     (with-temp-public-card [{uuid :public_uuid}]
       (set (keys (http/client :get 200 (str "public/card/" uuid)))))))
 
+(tu/resolve-private-vars metabase.api.public public-card)
+
+;; make sure :param_values get returned as expected
+(expect
+  {(data/id :categories :name) {:values                75
+                                :human_readable_values {}
+                                :field_id              (data/id :categories :name)}}
+  (tt/with-temp Card [card {:dataset_query {:type   :native
+                                            :native {:query         "SELECT COUNT(*) FROM venues LEFT JOIN categories ON venues.category_id = categories.id WHERE {{category}}"
+                                                     :collection    "CATEGORIES"
+                                                     :template_tags {:category {:name         "category"
+                                                                                :display_name "Category"
+                                                                                :type         "dimension"
+                                                                                :dimension    ["field-id" (data/id :categories :name)]
+                                                                                :widget_type  "category"
+                                                                                :required     true}}}}}]
+    (-> (:param_values (public-card :id (u/get-id card)))
+        (update-in [(data/id :categories :name) :values] count))))
+
 
 ;;; ------------------------------------------------------------ GET /api/public/card/:uuid/query (and JSON and CSV versions)  ------------------------------------------------------------
 
diff --git a/test/metabase/driver_test.clj b/test/metabase/driver_test.clj
index ecd718263635c0ecf0ad168bf5d60a2595f816f3..a2651796dbf7225bbe69c1d912acf3f4b14fb517 100644
--- a/test/metabase/driver_test.clj
+++ b/test/metabase/driver_test.clj
@@ -1,6 +1,6 @@
 (ns metabase.driver-test
   (:require [expectations :refer :all]
-            [metabase.driver :refer :all]))
+            [metabase.driver :as driver]))
 
 
 (defrecord TestDriver []
@@ -8,11 +8,11 @@
   (getName [_] "TestDriver"))
 
 (extend TestDriver
-  IDriver
+  driver/IDriver
   {:features (constantly #{:a})})
 
 
 ;; driver-supports?
 
-(expect true (driver-supports? (TestDriver.) :a))
-(expect false (driver-supports? (TestDriver.) :b))
+(expect true  (driver/driver-supports? (TestDriver.) :a))
+(expect false (driver/driver-supports? (TestDriver.) :b))
diff --git a/test/metabase/models/metric_test.clj b/test/metabase/models/metric_test.clj
index e0ec8b92ad3b5890176e13907770b700fa54b6a9..4ac4e9a7e4b793dd630d8b7f827bf5dc140d4541 100644
--- a/test/metabase/models/metric_test.clj
+++ b/test/metabase/models/metric_test.clj
@@ -10,6 +10,15 @@
             [metabase.test.util :as tu]
             [metabase.util :as u]))
 
+(def ^:private ^:const metric-defaults
+  {:description             nil
+   :how_is_this_calculated  nil
+   :show_in_getting_started false
+   :caveats                 nil
+   :points_of_interest      nil
+   :is_active               true
+   :definition              {}})
+
 (defn- user-details
   [username]
   (dissoc (fetch-user username) :date_joined :last_login))
@@ -30,16 +39,11 @@
 
 ;; create-metric!
 (expect
-  {:creator_id              (user->id :rasta)
-   :creator                 (user-details :rasta)
-   :name                    "I only want *these* things"
-   :description             nil
-   :how_is_this_calculated  nil
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :is_active               true
-   :definition              {:clause ["a" "b"]}}
+  (merge metric-defaults
+         {:creator_id (user->id :rasta)
+          :creator    (user-details :rasta)
+          :name       "I only want *these* things"
+          :definition {:clause ["a" "b"]}})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{:keys [id]}      {:db_id database-id}]]
     (create-metric-then-select! id "I only want *these* things" nil (user->id :rasta) {:clause ["a" "b"]})))
@@ -60,17 +64,13 @@
 
 ;; retrieve-metric
 (expect
-  {:creator_id              (user->id :rasta)
-   :creator                 (user-details :rasta)
-   :name                    "Toucans in the rainforest"
-   :description             "Lookin' for a blueberry"
-   :how_is_this_calculated  nil
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :is_active               true
-   :definition              {:database 45
-                             :query    {:filter ["yay"]}}}
+  (merge metric-defaults
+         {:creator_id  (user->id :rasta)
+          :creator     (user-details :rasta)
+          :name        "Toucans in the rainforest"
+          :description "Lookin' for a blueberry"
+          :definition  {:database 45
+                        :query    {:filter ["yay"]}}})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id :id}    {:db_id database-id}]
                   Metric   [{metric-id :id}   {:table_id    table-id
@@ -81,18 +81,12 @@
               :creator (u/rpartial dissoc :date_joined :last_login)))))
 
 
-;; retrieve-segements
+;; retrieve-metrics
 (expect
-  [{:creator_id              (user->id :rasta)
-    :creator                 (user-details :rasta)
-    :name                    "Metric 1"
-    :description             nil
-    :how_is_this_calculated  nil
-    :show_in_getting_started false
-    :caveats                 nil
-    :points_of_interest      nil
-    :is_active               true
-    :definition              {}}]
+  [(merge metric-defaults
+          {:creator_id (user->id :rasta)
+           :creator    (user-details :rasta)
+           :name       "Metric 1"})]
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id-1 :id}    {:db_id database-id}]
                   Table    [{table-id-2 :id}    {:db_id database-id}]
@@ -113,17 +107,12 @@
 ;;  4. ability to modify the definition json
 ;;  5. revision is captured along with our commit message
 (expect
-  {:creator_id              (user->id :rasta)
-   :creator                 (user-details :rasta)
-   :name                    "Costa Rica"
-   :description             nil
-   :how_is_this_calculated  nil
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :is_active               true
-   :definition              {:database 2
-                             :query    {:filter ["not" "the toucans you're looking for"]}}}
+  (merge metric-defaults
+         {:creator_id (user->id :rasta)
+          :creator    (user-details :rasta)
+          :name       "Costa Rica"
+          :definition {:database 2
+                       :query    {:filter ["not" "the toucans you're looking for"]}}})
   (tt/with-temp* [Database [{database-id :id}]
                   Table  [{table-id :id}  {:db_id database-id}]
                   Metric [{metric-id :id} {:table_id table-id}]]
@@ -142,16 +131,12 @@
 
 ;; delete-metric!
 (expect
-  {:creator_id              (user->id :rasta)
-   :creator                 (user-details :rasta)
-   :name                    "Toucans in the rainforest"
-   :description             "Lookin' for a blueberry"
-   :how_is_this_calculated  nil
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :is_active               false
-   :definition              {}}
+  (merge metric-defaults
+         {:creator_id  (user->id :rasta)
+          :creator     (user-details :rasta)
+          :name        "Toucans in the rainforest"
+          :description "Lookin' for a blueberry"
+          :is_active   false})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id :id}  {:db_id database-id}]
                   Metric   [{metric-id :id} {:table_id table-id}]]
@@ -165,18 +150,14 @@
 
 ;; serialize-metric
 (expect
-  {:id                      true
-   :table_id                true
-   :creator_id              (user->id :rasta)
-   :name                    "Toucans in the rainforest"
-   :description             "Lookin' for a blueberry"
-   :how_is_this_calculated  nil
-   :show_in_getting_started false
-   :caveats                 nil
-   :points_of_interest      nil
-   :definition              {:aggregation ["count"]
-                             :filter      ["AND" [">" 4 "2014-10-19"]]}
-   :is_active               true}
+  (merge metric-defaults
+         {:id          true
+          :table_id    true
+          :creator_id  (user->id :rasta)
+          :name        "Toucans in the rainforest"
+          :description "Lookin' for a blueberry"
+          :definition  {:aggregation ["count"]
+                        :filter      ["AND" [">" 4 "2014-10-19"]]}})
   (tt/with-temp* [Database [{database-id :id}]
                   Table    [{table-id :id} {:db_id database-id}]
                   Metric   [metric         {:table_id   table-id
diff --git a/test/metabase/permissions_collection_test.clj b/test/metabase/permissions_collection_test.clj
index d6e82ec37120919812faf7c81eb07854a1964be5..7e5c19c7abd71024bba8cf5b3ca5d58e918c5a7d 100644
--- a/test/metabase/permissions_collection_test.clj
+++ b/test/metabase/permissions_collection_test.clj
@@ -55,8 +55,10 @@
     (println "[In the occasionally failing test]") ; DEBUG
     (set-card-collection! collection)
     (permissions/grant-collection-read-permissions! (group/all-users) collection)
-    ;; try it twice because sometimes it randomly fails :unamused:
+    ;; try it a few times because sometimes it randomly fails :unamused:
     (or (can-run-query? :rasta)
+        (can-run-query? :rasta)
+        (Thread/sleep 1000)
         (can-run-query? :rasta))))
 
 ;; Make sure a User isn't allowed to save a Card they have collections readwrite permissions for
diff --git a/test/metabase/test/data/oracle.clj b/test/metabase/test/data/oracle.clj
index f7ef1d3dc2ca2928fdee2f6d134fa9f56eb38616..31893cea4cf86a7091b29341754715e0edfdcad8 100644
--- a/test/metabase/test/data/oracle.clj
+++ b/test/metabase/test/data/oracle.clj
@@ -86,11 +86,13 @@
 
   i/IDatasetLoader
   (merge generic/IDatasetLoaderMixin
-         {:database->connection-details (fn [& _] @db-connection-details)
-          :default-schema               (constantly session-schema)
-          :engine                       (constantly :oracle)
-          :expected-base-type->actual   (u/drop-first-arg expected-base-type->actual)
-          :id-field-type                (constantly :type/Decimal)}))
+         {:database->connection-details       (fn [& _] @db-connection-details)
+          :default-schema                     (constantly session-schema)
+          :engine                             (constantly :oracle)
+          :expected-base-type->actual         (u/drop-first-arg expected-base-type->actual)
+          :id-field-type                      (constantly :type/Decimal)
+          ;; TODO - Not sure why we need to do this
+          :has-questionable-timezone-support? (constantly true)}))
 
 (defn- dbspec [& _]
   (sql/connection-details->spec (OracleDriver.) @db-connection-details))
diff --git a/test/metabase/util/schema_test.clj b/test/metabase/util/schema_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..ed4ab0f7d385cfb0503f61fadaf3490c9fe38284
--- /dev/null
+++ b/test/metabase/util/schema_test.clj
@@ -0,0 +1,9 @@
+(ns metabase.util.schema-test
+  (:require [metabase.util.schema :as su]
+            [schema.core :as s]
+            [expectations :refer :all]))
+
+;; check that the API error message generation is working as intended
+(expect
+  "value may be nil, or if non-nil, value must satisfy one of the following requirements: 1) value must be a boolean. 2) value must be a valid boolean string ('true' or 'false')."
+  (su/api-error-message (s/maybe (s/cond-pre s/Bool su/BooleanString))))
diff --git a/test/metabase/util_test.clj b/test/metabase/util_test.clj
index 8520c9866e9978d35e3ba369980f980ee988d8ef..8232181092db3bae876e196c3de9d437f5994a29 100644
--- a/test/metabase/util_test.clj
+++ b/test/metabase/util_test.clj
@@ -230,3 +230,15 @@
 (expect 1 (occurances-of-substring "WHERE ID = {{id}}"                                                                 "{{id}}"))
 (expect 2 (occurances-of-substring "WHERE ID = {{id}} OR USER_ID = {{id}}"                                             "{{id}}"))
 (expect 3 (occurances-of-substring "WHERE ID = {{id}} OR USER_ID = {{id}} OR TOUCAN_ID = {{id}} OR BIRD_ID = {{bird}}" "{{id}}"))
+
+
+;;; tests for `select-non-nil-keys` and `select-keys-when`
+(expect
+  {:a 100}
+  (select-non-nil-keys {:a 100, :b nil} #{:a :b :c}))
+
+(expect
+  {:a 100, :b nil, :d 200}
+  (select-keys-when {:a 100, :b nil, :d 200, :e nil}
+    :present #{:a :b :c}
+    :non-nil #{:d :e :f}))
diff --git a/webpack.config.js b/webpack.config.js
index f7b38b38997673695b24deb82d4067c21bc47d0c..b8dcc707321c322884311816d6c28708768c0785 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -253,6 +253,7 @@ if (NODE_ENV !== "production") {
     // this is required to ensure we don't minify Chevrotain token identifiers
     // https://github.com/SAP/chevrotain/tree/master/examples/parser/minification
     config.plugins.push(new webpack.optimize.UglifyJsPlugin({
+        warnings: false,
         mangle: {
             except: allTokens.map(function(currTok) {
                 return chevrotain.tokenName(currTok);
diff --git a/yarn.lock b/yarn.lock
index c27c42036b63606c6dae202b7084a59c9f4b5154..a97a5a0fb2d45efdf15df0f0ab5c4ab4d5e85960 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3254,6 +3254,10 @@ har-validator@~4.2.1:
     ajv "^4.9.1"
     har-schema "^1.0.5"
 
+harmony-reflect@^1.4.6:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.5.1.tgz#b54ca617b00cc8aef559bbb17b3d85431dc7e329"
+
 has-ansi@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
@@ -3482,6 +3486,12 @@ icss-replace-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5"
 
+identity-obj-proxy@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
+  dependencies:
+    harmony-reflect "^1.4.6"
+
 ieee754@^1.1.4:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
@@ -6337,7 +6347,7 @@ react-sortable@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849"
 
-react-test-renderer@^15.4.2:
+react-test-renderer@^15.5.4:
   version "15.5.4"
   resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.5.4.tgz#d4ebb23f613d685ea8f5390109c2d20fbf7c83bc"
   dependencies: